Compare commits
43
Commits
v0.1.2
..
d541b04224
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d541b04224 | ||
|
|
c5b9cadbbd | ||
|
|
ab24ccc32b | ||
|
|
f0478c6d6b | ||
|
|
5a7e319caf | ||
|
|
06d08e2972 | ||
|
|
1bd6181585 | ||
|
|
0a1db52baf | ||
|
|
5ca44d16b1 | ||
|
|
7da2f90e0a | ||
|
|
7ebfc510af | ||
|
|
d5d5480eed | ||
|
|
4b2806ead5 | ||
|
|
6efb989ed9 | ||
|
|
0ed462d157 | ||
|
|
b4c0589745 | ||
|
|
6c33c770db | ||
|
|
4e3855fc35 | ||
|
|
6c858a8858 | ||
|
|
aae23d86f0 | ||
|
|
ebc9ca872c | ||
|
|
fe295aa73a | ||
|
|
e8e68a7ed6 | ||
|
|
77b211186c | ||
|
|
14174a7bb3 | ||
|
|
78f01066d2 | ||
|
|
7f12327c37 | ||
|
|
a29cbf6aae | ||
|
|
a441b08378 | ||
|
|
ea7beea863 | ||
|
|
18518e0d2d | ||
|
|
0b23fc3013 | ||
|
|
fcdc805f6c | ||
|
|
c132770fbb | ||
|
|
951f1a9a20 | ||
|
|
329d66e767 | ||
|
|
90edab6e33 | ||
|
|
b24a686188 | ||
|
|
1ee31e3b49 | ||
|
|
a1b0e99bdd | ||
|
|
0fd148ffbb | ||
|
|
33cc4f3e0c | ||
|
|
d861de5ead |
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Неверное поведение
|
||||
---
|
||||
## Проблема и шаги воспроизведения
|
||||
|
||||
## Факт / ожидание
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Окружение / ссылки (если применимо)
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Engineering Task
|
||||
about: CI, тесты, документация, процесс и прочее
|
||||
---
|
||||
## Контекст и результат
|
||||
|
||||
## Scope / связанные Issues (если есть)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Новое наблюдаемое поведение
|
||||
---
|
||||
## Зачем и какой результат
|
||||
|
||||
## Ожидаемое поведение
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Зависимости / связанные Issues (если есть)
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Refactoring / Technical Debt
|
||||
about: Улучшение без нового поведения
|
||||
---
|
||||
## Проблема и результат
|
||||
|
||||
## Сохраняемые инварианты / границы
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Зависимости / ссылки (если есть)
|
||||
@@ -0,0 +1,11 @@
|
||||
## Summary
|
||||
|
||||
## Linked Issue
|
||||
Refs
|
||||
|
||||
## Changes
|
||||
|
||||
## Verification
|
||||
- Команда / результат / ограничения:
|
||||
|
||||
## Breaking Changes (если есть)
|
||||
@@ -4,6 +4,10 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
concurrency:
|
||||
group: specs-commits-${{ gitea.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
commits:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -23,4 +27,8 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
|
||||
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
|
||||
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
|
||||
else
|
||||
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
|
||||
fi
|
||||
|
||||
@@ -2,12 +2,16 @@ name: Specs security
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
branches: [develop, main]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
schedule:
|
||||
- cron: '17 5 * * *'
|
||||
|
||||
concurrency:
|
||||
group: specs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -2,8 +2,13 @@ name: Specs verify
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
branches: [develop, main]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: specs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
npx --no-install commitlint --edit "$1" --strict
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
Репозиторий содержит существующие behavioral specifications и SDD history.
|
||||
Для обычной разработки не создавай и не обновляй planning artifacts ради
|
||||
процесса. Требование пользователя определяет работу; OpenSpec применяется
|
||||
только по явному запросу.
|
||||
процесса. До edits найди/создай подходящую Gitea Issue, проверь AC и ветку;
|
||||
чтение допускается без Issue. Выбор lightweight/SDD — по
|
||||
[Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md)
|
||||
(main#28; после merge — актуальная версия `develop`);
|
||||
OpenSpec lifecycle начинается только после явного решения Change Owner.
|
||||
|
||||
Если нужна существующая behavioral spec, выбери точный target через
|
||||
[AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI:
|
||||
@@ -16,11 +19,19 @@ anchors и `git diff --check`; application gates принадлежат affected
|
||||
Если явно запрошена работа через OpenSpec, следуй
|
||||
[локальным инструкциям OpenSpec](README.md#начало-работы)
|
||||
и используй store `wealth-ledger-specs`. Перед Node-командами активируй
|
||||
местную `.nvmrc` через `nvm use --silent`.
|
||||
местную `.nvmrc`: если `nvm` не загружен в non-interactive shell, загрузи
|
||||
установленный `$HOME/.nvm/nvm.sh`, затем выполни `nvm use --silent` из specs.
|
||||
Если `nvm.sh` недоступен, остановись; не используй системный Node.
|
||||
|
||||
## Ветки и релизы
|
||||
|
||||
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
|
||||
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
|
||||
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
|
||||
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
|
||||
не пушить. PR сливается squash или rebase-merge; merge-коммит не используется.
|
||||
`main` обновляется только fast-forward из `develop`, поэтому `origin/main`
|
||||
всегда предок `origin/develop`:
|
||||
`git merge-base --is-ancestor origin/main origin/develop`. Срочный fix —
|
||||
обычная ветка `fix/*` от `develop`; пути hotfix от `main` нет.
|
||||
Релиз — по [README](README.md#релизы).
|
||||
Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release);
|
||||
для спецификаций допустим `spec:`.
|
||||
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
|
||||
|
||||
@@ -16,8 +16,13 @@ types. CI каждого участника использует только е
|
||||
| --- | --- |
|
||||
| Auth/session/refresh/error contract | [User authentication](openspec/specs/user-authentication/spec.md) |
|
||||
| T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank-connection/spec.md) |
|
||||
| Владелец синхронизации T-Bank, scheduling/priority | [T-Bank data sync](openspec/specs/tbank-data-sync/spec.md) |
|
||||
| Часовой обход, результат последней попытки | [Hourly investment sync](openspec/specs/hourly-investment-sync/spec.md) |
|
||||
| Счета/сводки | [Brokerage accounts](openspec/specs/brokerage-accounts/spec.md) |
|
||||
| Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio-view/spec.md) |
|
||||
| История стоимости Portfolio | [Portfolio value history](openspec/specs/portfolio-value-history/spec.md) |
|
||||
| Справочные сведения/поиск инструментов (online-only) | [Online instrument information](openspec/specs/online-instrument-information/spec.md) |
|
||||
| Ручные дебетовые карты | [Manual bank cards](openspec/specs/manual-bank-cards/spec.md) |
|
||||
| Операции/cursor/filters | [Account ledger](openspec/specs/brokerage-account-ledger/spec.md) |
|
||||
| Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) |
|
||||
| Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) |
|
||||
@@ -25,6 +30,7 @@ types. CI каждого участника использует только е
|
||||
| Каталог компонентов | [UI catalog](openspec/specs/frontend-ui-catalog/spec.md) |
|
||||
| Loading/skeleton | [Loading presentation](openspec/specs/frontend-loading-presentation/spec.md) |
|
||||
| UI error/recovery | [UI resilience](openspec/specs/frontend-ui-resilience/spec.md) |
|
||||
| Тихое чтение и редкие уведомления | [User feedback](openspec/specs/frontend-user-feedback/spec.md) |
|
||||
| Query coherence/cache | [Query coherence](openspec/specs/frontend-query-coherence/spec.md), [data resilience](openspec/specs/frontend-data-resilience/spec.md) |
|
||||
| OpenAPI generation | [API contract generation](openspec/specs/api-contract-generation/spec.md) |
|
||||
| Logging/request context | [Structured logging](openspec/specs/backend-structured-logging/spec.md) |
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.5](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.4...v0.1.5) (2026-10-18)
|
||||
|
||||
### Specifications
|
||||
|
||||
* add five capability entries to AI_INDEX (WealthLedger/specs[#28](https://git.personal.ksv741.ru/WealthLedger/specs/issues/28)) ([d5d5480](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d5d5480eedbc25c77c52b3b477de37e373beb8d9))
|
||||
* align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs[#46](https://git.personal.ksv741.ru/WealthLedger/specs/issues/46)) ([#47](https://git.personal.ksv741.ru/WealthLedger/specs/issues/47)) ([1bd6181](https://git.personal.ksv741.ru/WealthLedger/specs/commit/1bd618158588871f1801bfc6bead82fba31ce0ae))
|
||||
* archive quiet-user-feedback and sync canonical specs ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([f0478c6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/f0478c6d6b5a5f259ea766276295758b34d240b3))
|
||||
* archive three completed changes (main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33), frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63), screener) ([6efb989](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6efb989ed9051981a314c8eb5714dffa88195a80))
|
||||
* clarify SDD applies only on explicit request (WealthLedger/specs[#1](https://git.personal.ksv741.ru/WealthLedger/specs/issues/1)) ([7ebfc51](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7ebfc510afb7440f85ecf95e2891d84c1b8ad843))
|
||||
* fix upcoming events date filter default and picker bounds (WealthLedger/frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63)) ([b4c0589](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b4c0589745e0d1fb6b482addbea82d06c4924dad))
|
||||
* reconcile quiet-user-feedback with source-state requirements ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([5a7e319](https://git.personal.ksv741.ru/WealthLedger/specs/commit/5a7e319cafd8d9b6861b5a6bab7085bb57b08576))
|
||||
* unify income payment cards and drop period comparisons (WealthLedger/main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33)) ([#37](https://git.personal.ksv741.ru/WealthLedger/specs/issues/37)) ([6c858a8](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6c858a8858dfdfc5c9607b3f925de5900ef1c903))
|
||||
|
||||
### CI
|
||||
|
||||
* rerun commits proof on PR title edit (WealthLedger/specs[#30](https://git.personal.ksv741.ru/WealthLedger/specs/issues/30)) ([4e3855f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/4e3855fc3526e2b0b7ba80fd23b996d9993edcf5))
|
||||
|
||||
### Maintenance
|
||||
|
||||
* adopt linear git model in specs instructions ([#48](https://git.personal.ksv741.ru/WealthLedger/specs/issues/48)) ([06d08e2](https://git.personal.ksv741.ru/WealthLedger/specs/commit/06d08e29724e6cbe95eeca5ee1e6af7ec365a559))
|
||||
* make OpenSpec toolchain explicit and diagnosable (WealthLedger/specs[#4](https://git.personal.ksv741.ru/WealthLedger/specs/issues/4)) ([#43](https://git.personal.ksv741.ru/WealthLedger/specs/issues/43)) ([7da2f90](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7da2f90e0a6a7e0e5cb40b371c965a772aaa5db2))
|
||||
|
||||
## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.3...v0.1.4) (2026-10-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* align specs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a29cbf6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a29cbf6aae7ebe48a51ae1979ff3f30b9fa8e305))
|
||||
|
||||
### Documentation
|
||||
|
||||
* clarify specs Node bootstrap ([18518e0](https://git.personal.ksv741.ru/WealthLedger/specs/commit/18518e0d2daecb843fea1b4d55d1c7d2cabf24f6))
|
||||
* pin specs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([7f12327](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f12327c37055ba903f962b1220138da9c7edd06))
|
||||
|
||||
### Specifications
|
||||
|
||||
* add harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([14174a7](https://git.personal.ksv741.ru/WealthLedger/specs/commit/14174a7bb335152c8cc76d9a6bbaefaaaa58b22f))
|
||||
* add stock screener ([951f1a9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/951f1a9a20a09695e2ff85d1ee96632e98a202d8))
|
||||
* address hourly-sync review findings (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([77b2111](https://git.personal.ksv741.ru/WealthLedger/specs/commit/77b211186c297df4187e044804786ffc9096ce19))
|
||||
* archive harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([fe295aa](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fe295aa73ab5bc9d826067892930b7161fb4f1b9))
|
||||
* define hourly portfolio sync history ([b24a686](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b24a6861887e831ef2d73a97f36cc213be2be0b8))
|
||||
* finalize wealth-ledger SDD migration ([329d66e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/329d66e767f7e89a40b96bb8e5b499f39f67feb6))
|
||||
|
||||
### CI
|
||||
|
||||
* avoid duplicate PR checks ([c132770](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c132770fbb4a4e48cae0a4ceca7407bb6a768ded))
|
||||
* skip checks on PR metadata edits ([fcdc805](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121))
|
||||
|
||||
### Maintenance
|
||||
|
||||
* align specs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a441b08](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a441b083785d8f50c42dea543e684b23f27732a2))
|
||||
|
||||
## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.2...v0.1.3) (2026-10-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** patch release tooling advisories ([33cc4f3](https://git.personal.ksv741.ru/WealthLedger/specs/commit/33cc4f3e0c20ed3d6f5814cdc609724ffb8564ac))
|
||||
|
||||
### Specifications
|
||||
|
||||
* add reduce-screen-sync-polling change ([a1b0e99](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a1b0e99bddb91f7b7ca729ddf6b7d6c688fdca41))
|
||||
* simplify runtime requirements ([d861de5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d861de5ead41b4977083b4aee08fc668c35e5b34))
|
||||
|
||||
### Maintenance
|
||||
|
||||
* run commitlint in commit-msg hook ([0fd148f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0fd148ffbb39ea883a7e7799aa1b87c93b31fa17))
|
||||
|
||||
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.1...v0.1.2) (2026-09-26)
|
||||
|
||||
### Specifications
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
# WealthLedger Specs
|
||||
|
||||
Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace), новые changes используют project-local schema `wealth-ledger-sdd`.
|
||||
Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace). Новые changes используют `wealth-ledger-sdd-v2`; существующие active changes с `schema: wealth-ledger-sdd` завершаются по legacy schema без автоматической миграции.
|
||||
|
||||
## Начало работы
|
||||
|
||||
OpenSpec применяется по явному запросу согласно [AGENTS](AGENTS.md). Proposal, Delta Specs, Design и Tasks создаются последовательно по одному после approval Change Owner. Для любой команды, читающей или изменяющей change, явно передавайте `--store wealth-ledger-specs`.
|
||||
OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
|
||||
|
||||
Design должен оценить `frontend`, `backend`, `specs`, `docs` в Delivery Map и при обязательном trigger включить Solution Assessment. Docs описывает as-built state и не становится параллельным store требований.
|
||||
| Change | Lifecycle |
|
||||
| --- | --- |
|
||||
| Active `schema: wealth-ledger-sdd` | Proposal → Delta Specs → Design → Tasks → Apply; действующие approvals сохраняются. Legacy Design оценивает `frontend`, `backend`, `specs`, `docs` в Delivery Map. |
|
||||
| New `schema: wealth-ledger-sdd-v2` | Proposal → Delta Specs → System Analysis → Design + QA Analysis → Docs Impact → Tasks → Apply. Design и QA Analysis готовы после System Analysis; Docs Impact ждёт обоих. |
|
||||
|
||||
Для changes без изменения observable behavior используется `skip_specs: true`. В v2 System Analysis/QA Analysis/Docs Impact всё равно фиксируют только применимое содержание и обоснованное N/A, а не фиктивные contracts или tests. `specs/openspec/specs/` хранит нормативное поведение, `docs/` — текущий AS-IS, backend-generated OpenAPI — текущий executable HTTP contract. Target contracts остаются в change до архива; docs обновляются только по согласованному Docs Impact через `[docs]` tasks. После завершения последнего legacy change эту переходную заметку можно удалить отдельным изменением.
|
||||
|
||||
## Проверка
|
||||
|
||||
Для native validation из этого репозитория:
|
||||
|
||||
```bash
|
||||
npm run check:openspec
|
||||
openspec schema validate wealth-ledger-sdd --verbose
|
||||
openspec schema validate wealth-ledger-sdd-v2 --verbose
|
||||
openspec status --change <change> --store wealth-ledger-specs
|
||||
openspec validate <change> --type change --strict --store wealth-ledger-specs
|
||||
openspec validate --specs --strict --store wealth-ledger-specs
|
||||
```
|
||||
|
||||
Structural validation не заменяет semantic verification и independent read-only review перед archive.
|
||||
OpenSpec CLI ставится глобально и не закрепляется в `package.json`: его транзитивная
|
||||
зависимость `braces` имеет advisory без исправления в 3.x. Ожидаемая версия хранится
|
||||
в `openspec/.cli-version`; `npm run check:openspec` проверяет Node из `.nvmrc`, версию
|
||||
CLI и доступность store, и при расхождении выводит команду исправления. Structural
|
||||
validation не заменяет semantic verification и independent read-only review перед archive.
|
||||
|
||||
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
|
||||
`npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой
|
||||
@@ -27,35 +39,60 @@ Node задаётся собственной `.nvmrc`; после `nvm use --sil
|
||||
## Релизы
|
||||
|
||||
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
|
||||
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
|
||||
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
|
||||
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
|
||||
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в
|
||||
`develop`; общие правила — в Engineering Workflow по ссылке из AGENTS.
|
||||
PR сливается squash или rebase-merge; merge-коммит не используется. `main`
|
||||
обновляется только fast-forward из `develop`, поэтому история линейна:
|
||||
|
||||
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
|
||||
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
|
||||
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
|
||||
`release/hotfix-<name>` и проходит тот же процесс.
|
||||
```text
|
||||
<type>/<issue>-<slug> --squash--> develop --ff-only--> main --> tag vX.Y.Z
|
||||
```
|
||||
|
||||
Инвариант: `origin/main` всегда предок `origin/develop`. Проверка:
|
||||
|
||||
```bash
|
||||
git fetch -q origin
|
||||
git merge-base --is-ancestor origin/main origin/develop && echo ok || echo BROKEN
|
||||
```
|
||||
|
||||
Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release),
|
||||
включая `spec:`. Версии вручную не повышай.
|
||||
|
||||
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Проверь
|
||||
инвариант. Создай `release/X.Y.Z` от проверенного `develop`;
|
||||
`nvm use --silent`, `npm ci`.
|
||||
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
|
||||
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
|
||||
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
|
||||
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
|
||||
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
|
||||
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
|
||||
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
|
||||
изменился, согласуй историю и повтори проверки кандидата.
|
||||
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
||||
открой PR в **`develop`** с заголовком `chore(release): X.Y.Z`. После зелёных
|
||||
`verify`, `security`, `commits` выполни fast-forward-only merge. Если
|
||||
`develop` изменился, обнови release-ветку и повтори проверки кандидата.
|
||||
4. Открой PR `develop → main` с тем же заголовком и слей fast-forward-only.
|
||||
5. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
||||
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
|
||||
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
|
||||
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
|
||||
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
|
||||
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
|
||||
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
|
||||
существующий кандидат без повторного повышения версии.
|
||||
|
||||
Обратная синхронизация не нужна: release-коммит попал в `develop` на шаге 3.
|
||||
Опубликованные теги не перемещай. При сбое после merge создай отсутствующий тег
|
||||
без нового bump; до merge исправь и проверь существующий кандидат без
|
||||
повторного повышения версии.
|
||||
|
||||
Freeze релиза не поддерживается: пока открыт релизный PR, `develop` не двигают.
|
||||
Срочный fix — обычная ветка `fix/*` от `develop` с последующим релизом;
|
||||
пути от `main` нет, он сделал бы `main` не-предком `develop`. Исключение
|
||||
требует явного решения: `release/hotfix-<name>` от `main`, PR в `main`, тег,
|
||||
затем merge `main → develop` для восстановления инварианта.
|
||||
|
||||
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
|
||||
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
|
||||
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
|
||||
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
|
||||
администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены.
|
||||
администратором, включена блокировка устаревшей ветки, обязательны `verify`,
|
||||
`security`, `commits`; теги `v*` защищены. Линейность обеспечивают merge styles
|
||||
репозитория: merge-коммиты и rebase `--no-ff` выключены, доступны squash,
|
||||
rebase-merge и fast-forward-only; стиль по умолчанию — squash.
|
||||
Workflow сам не включает эти настройки. Отдельное review approval для одного
|
||||
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
|
||||
defaultIgnores: false,
|
||||
ignores: [
|
||||
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
|
||||
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
|
||||
],
|
||||
rules: {
|
||||
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
|
||||
"references-empty": [2, "never"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.10.0
|
||||
1.13.1
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Completion Evidence: add-manual-bank-cards
|
||||
|
||||
## Finalization Authority
|
||||
|
||||
Change Owner authorized Finalization, canonical sync and archive after a current
|
||||
independent read-only Validation PASS. The PASS found no Critical or Important
|
||||
findings after the monthly-summary presentation correction.
|
||||
|
||||
## Delivery
|
||||
|
||||
| Area | Result | Delivery commit |
|
||||
| --- | --- | --- |
|
||||
| frontend | Manual card screens and summaries; Finalization correction shows exact expenses, refunds and net totals with a regression test. | `ff67a76`, `d3ac8c2`, `734c5bd` |
|
||||
| backend | Bank-card persistence, calculations, API and isolated verification. | `e6f58ef` |
|
||||
| docs | Cards overview states the three displayed summary totals. | `3a27268` |
|
||||
| specs | Approved legacy Proposal, Delta Specs, Design, completed Tasks and Finalization evidence; native sync creates canonical behavior spec. | `834f2d1` plus finalization/archive commit |
|
||||
|
||||
## Observed Verification
|
||||
|
||||
| Area | Command or review | Result |
|
||||
| --- | --- | --- |
|
||||
| frontend | `npm run test:run -- src/entities/bank-card/ui/BankSummary/BankSummary.view.test.tsx` | PASS: 1 file, 1 test |
|
||||
| frontend | `npm run typecheck` | PASS |
|
||||
| docs | `npm run verify` | PASS |
|
||||
| specs | `openspec schema validate wealth-ledger-sdd --verbose` | PASS |
|
||||
| specs | `openspec schema validate wealth-ledger-sdd-v2 --verbose` | PASS |
|
||||
| specs | `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs` | PASS |
|
||||
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 25 specs, 0 failed |
|
||||
| all affected areas | `git diff --check` | PASS |
|
||||
|
||||
## Independent Validation
|
||||
|
||||
PASS. The review compared Proposal, all 17 Delta Requirements, Design and
|
||||
completed Tasks with current backend calculations/DTOs, shared frontend summary
|
||||
rendering and cards documentation. It verified that `/cards` and `/cards/:id`
|
||||
share the corrected summary view and found no Critical or Important findings.
|
||||
|
||||
Known non-blocking limitation: a plain Prisma fresh migration deploy still needs
|
||||
`RUST_LOG=info`; the verified migration path and its evidence remain recorded in
|
||||
the change Tasks.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Completion Evidence: containerized-application-delivery
|
||||
|
||||
## Outcome
|
||||
|
||||
The local and disposable single-server container delivery path is implemented
|
||||
and verified. External publication and target-server gates remain unavailable in
|
||||
the current environment and are recorded as blockers rather than represented as
|
||||
successful results.
|
||||
|
||||
## Observed Results
|
||||
|
||||
- Frontend and backend development/production images build and run locally.
|
||||
- Development HMR/watch, same-origin API routing, production SPA fallback,
|
||||
readiness, persistence, migration failure handling, backup and isolated
|
||||
restore were verified on disposable data.
|
||||
- Docs verification and Compose configuration checks passed.
|
||||
- No production data, secrets, registry credentials or real server were used.
|
||||
|
||||
## External Blockers
|
||||
|
||||
The following approved implementation tasks remain externally blocked:
|
||||
|
||||
- Docker-capable Gitea runner acceptance;
|
||||
- registry publisher and server pull-only credentials;
|
||||
- authenticated candidate publication and digest pull;
|
||||
- target-server Docker/Compose verification;
|
||||
- first server rollout pair confirmation.
|
||||
|
||||
These tasks remain unchecked in the archived checklist. The change is archived
|
||||
as externally blocked after owner authorization; this is not a claim that the
|
||||
external gates passed and does not authorize production rollout.
|
||||
+1
-1
@@ -92,7 +92,7 @@ secrets и опубликованные release tags не используютс
|
||||
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`.
|
||||
- **Verification:** dry-run runbook на disposable data проходит без неописанного шага; frontend-only и backend-only paths не пересоздают другой component; restore требует отдельного решения и сообщает о потере данных после backup.
|
||||
|
||||
- [ ] 5.3 [docs] Выполнить docs repository completion gate.
|
||||
- [x] 5.3 [docs] Выполнить docs repository completion gate.
|
||||
- **Covers:** `N/A` — repository-owned проверка durable as-built documentation и ссылок без нового runtime behavior.
|
||||
- **Verification:** `npm ci`, `npm run verify`, `git diff --check`; проверить Mermaid rendering, взаимные ADR/architecture/runbook links и отсутствие credentials или host-private values.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
schema: wealth-ledger-sdd
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-10-07
|
||||
@@ -0,0 +1,96 @@
|
||||
## Context
|
||||
|
||||
Backend — один NestJS process и SQLite. Сейчас screen-driven синхронизация раскладывает запросы экранов на source work; владельцы membership, portfolio, operations, forecast, catalog и candles используют reservations, attempt/revision fences и короткие транзакции без RPC внутри них. Текущий portfolio snapshot перезаписывается и каскадно удаляется при замене подключения или исчезновении счёта. `TbankSyncService` принимает экранный POST; frontend `screen-sync` запускает его и наблюдает status. Справочные данные, результаты поиска и отрицательные lookup живут в сохранённых таблицах, а detail является зависимостью событий и рыночных данных. Новый режим требует смены инициатора, не второй конкурирующей системы записи.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** сохранить единственного владельца публикации каждого источника; сделать расписание устойчивым к рестарту в рамках одного процесса; связать точки истории с успешной публикацией портфеля и подтверждённым доступом; отделить сетевое чтение интерфейса от брокерского обновления; перевести подробные сведения инструментов на онлайн-источник без потери сохранённых рыночных и личных данных.
|
||||
|
||||
**Non-Goals:** график и публичная история точек, новый механизм синхронизации между процессами, повторное сканирование полной рыночной истории и уже полных операций, изменение банковских карт и auth-refresh.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Один durable clock поверх существующих source owners
|
||||
|
||||
В `tbank-sync` вводится одиночный backend scheduler с persisted последней попыткой для текущего подключения: начало, завершение/состояние и диагностические разделы без токенов. При успешном PUT подключения фоновой работе передаётся новая граница; при старте process scheduler сверяет её и последнее начало, не опираясь на frontend GET. Проверка часовым таймером только предлагает работу; атомарное резервирование сверяет user/revision, время начала предыдущей попытки и отсутствие ещё активной попытки. Во время работы новое резервирование не допускается; пересечённый срок пропускается, а не исполняется «вдогонку». После рестарта незавершённая попытка старого process становится прерванной, но её время начала остаётся основанием следующего запуска; новый process не публикует её поздние результаты. Успешное подключение начинает новую первую попытку, даже если прежний токен был заменён незадолго до срока. Штатный процесс инициализирует таймер и освобождает его при shutdown.
|
||||
|
||||
Scheduler делает один обход подтверждённого membership, затем даёт portfolio owner обновить каждый доступный счёт, operations owner — только изменяемый хвост, forecast/source owners — события бумаг состава. Передаёт доступ и ревизию владельцам, не пишет их личные наборы сам и не держит транзакцию при сетевых вызовах. Результаты отдельных источников дают общий итог `succeeded/partial/failed` и безопасные идентификаторы счёта/раздела. При недействительном токене применяется существующая блокировка личных ответов. Альтернатива — reuse screen intent с поддельным экраном `accounts`: он не покрывает операции и события для каждого счёта и случайно включает ненужные зависимости; выделение фонового orchestration над теми же owners точнее.
|
||||
|
||||
### История отдельно от revision, но внутри атомарной публикации
|
||||
|
||||
Состояние истории принадлежит `(userId, accountId)` и переживает смену token/revision; текущий snapshot и source work по-прежнему принадлежат `(userId, revision, accountId)`. Точки используют точную десятичную строку, ISO-валюту и фактическое время успешной публикации, без состава позиций. Проверка полноты оценки всех позиций предшествует вставке любых валютных точек; пустой безвалютный портфель не даёт строк. Для однозначности повторной публикации точка привязана к идентификатору успешной попытки portfolio, а её запись и замена snapshot происходят в одной короткой транзакции после conditional attempt claim. При потере revision/доступа вся транзакция отменяется. Неоценённый состав сохраняется текущим без исторических точек.
|
||||
|
||||
Видимость истории определяется текущим подтверждённым membership и признаком удаления счёта, а не одной лишь идентичностью исторических строк. Успешное подтверждение списка, успешный PUT нового token и DELETE обновляют пометки доступа вместе со сменой текущей границы; неуспешный PUT/GET membership не меняет их. Чтение возможной истории в будущем обязано проверять session, текущее неотклонённое подключение и активное подтверждение конкретного accountId: `deletedAt = null` само по себе не является полномочием. Это позволяет сохранить прошлые точки, не воскресить приватный снимок прежней revision и не раскрыть историю между отключением и повторным подтверждением. Поскольку endpoint истории сейчас не предусмотрен, API для её чтения не проектируется.
|
||||
|
||||
### Сначала разделить источники по смыслу, затем удалить screen import
|
||||
|
||||
Обычные GET инвестиционных областей становятся локальными для membership/portfolio и изменяемых операций/прогнозов; их freshness отражает час, а ошибки попыток живут отдельно. Исключения — первичная загрузка отсутствующего исторического интервала операций и впервые запрошенного диапазона свечей; покрытие остаётся владельцем операций/candles. Текущие quotes/незавершённые свечи обновляются по запросу не чаще часа. Удаление ручного screen POST и его наблюдения следует после перехода всех consumers на чистые reads и статус последнего обхода. Иначе нынешние `sync=observe` GET и retry-кнопка могут обходить новый график; транспортный retry сохраняется, но не вызывает брокерский импорт. Публичный DTO статуса описывает только последнюю попытку и читается под той же auth/access boundary. Backend публикует OpenAPI как контракт, frontend обновляет свой snapshot из него отдельно.
|
||||
|
||||
### Онлайн-справочник без разрушения зависимостей
|
||||
|
||||
Публичный instrument detail, company и catalog search идут через существующие T-Bank/MOEX/NCR clients по запросу с проверкой UID/ticker и сохранением различия «не найдено / неоднозначно / источник недоступен». Общая активная работа может коалесцировать одинаковые сетевые вызовы в процессе, но отдельная постоянная копия справочника и отрицательного lookup не создаётся. Для прогноза необходимые атрибуты запрашиваются внутри его работы; ошибка не заменяет последний успешный forecast. Для сохранённых quotes, candle coverage, событий и portfolio labels остаётся только минимальная стабильная identity, нужная для ключей и чтения; ссылки и пользовательские данные не зависят от удаляемого detail payload. Старые справочные записи выводятся из использования до рассмотрения их удаления; каскады не должны уничтожать свечи. При сбое онлайн-источника блок detail показывает отдельную ошибку, остальные сохранённые данные доступны в своих границах.
|
||||
|
||||
## UI/UX Applicability and Package
|
||||
|
||||
**Applicability and scope:** `new/changed` — на инвестиционных экранах убираются ручные команды синхронизации, добавляются 30-минутное перечитывание и статус последнего обхода на кабинете. Макет графика не нужен. Пакет полностью inline; локальные PNG/SVG не требуются.
|
||||
|
||||
**Package revision:** `1` (этот Design).
|
||||
|
||||
**Screens, states and interactions:**
|
||||
|
||||
| Экран / блок | Без результата | Данные сохранены | Сбой / частичный успех | Действие |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Кабинет/счета | штатная первая загрузка и отсутствие доступных счетов | прежние карточки и время обновления | сообщение о последнем обходе: состояние, время, названия проблемных счетов/разделов без token/error payload | переход к счёту или настройкам; кнопки синхронизации нет |
|
||||
| Портфель/операции/доход/события | локальная загрузка либо недостающий исторический период | сохранённые значения и исходное время остаются при перечитывании | server-side stale отдельно от transport error; прежние значения не становятся новым snapshot | повтор только HTTP-чтения при сетевой ошибке |
|
||||
| Карточка инструмента и поиск | справочный блок ожидает онлайн-ответ; свечи могут впервые загружаться отдельно | сохранённые позиции, операции, подписи, котировки/свечи и события сохраняют самостоятельные состояния | ошибка справочного блока не скрывает независимые данные; подпись snapshot не обещает свежие подробности | открытие и повтор HTTP-чтения сведений без команды синхронизации личных счетов |
|
||||
|
||||
30-минутное перечитывание действует только пока инвестиционный экран открыт; для онлайн-справочного блока оно также означает новый запрос сведений. Время последней попытки не подменяет `lastSuccessfulAt` отдельного счёта. Текст частичного успеха включает доступный список проблемных разделов; не показываются сырые upstream сообщения.
|
||||
|
||||
**Responsive behavior:** использовать существующие адаптивные контейнеры и ширины списков/карточек; строка статуса переносит названия счетов и разделов на узком экране, не закрывает содержимое и имеет доступное текстовое обозначение помимо цвета. Новых плотных таблиц, графиков или мобильной навигации нет.
|
||||
|
||||
**Reused source and differences:** существующие as-built страницы кабинета, портфеля, операций и карточек инструмента сохраняют компоновку; новый пакет не зависит от изменяемой внешней ссылки либо архивного макета. Отличия перечислены выше: статус обхода вместо ручного refresh и локальная ошибка online detail. Идентификатор immutable archived reuse не применяется: новый статус и новые переходы состояний описаны здесь inline, не объявляются точной копией прошлой версии.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Status | Architectural outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| frontend | `affected` | Убрать screen-triggered mutation и инвестиционные sync-кнопки, сохранить transport retry, добавить периодическое чтение и статус на кабинете, независимо показывать ошибку online detail. |
|
||||
| backend | `affected` | Durable scheduler, учёт частичного результата и доступа, точки истории, локальные read boundaries, онлайн-справочник и опубликованный API статуса. |
|
||||
| specs | `affected` | Нормативные delta requirements для нового расписания, истории и обновлённых инвестиционных/клиентских сценариев. |
|
||||
| docs | `affected` | Обновить объяснение свежести, областей и глоссарий, зафиксировать новое решение относительно ADR 017 без переписывания его исторического контекста. |
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
| Вариант | Fit и maturity | Security / data integrity | Operating cost и ограничения | Migration cost |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Внешний cron/worker с отдельным экземпляром | Зрелый для fleet, но дублирует приложение при single-process SQLite | Нужны распределённые lock/lease и граница токенов между процессами | Отдельный deployment и координация | Высокая; против текущей single-process модели |
|
||||
| Nest scheduling dependency | Зрелые cron decorators/timers, но сама библиотека не хранит результат попыток | В любом случае нужны persisted CAS/fences для restart и подключения | Ещё один runtime package и жизненный цикл без выигрыша от cron expression | Небольшая, но ненужная зависимость |
|
||||
| Native Node timer + существующие SQLite/Prisma reservations | Подходит для одного процесса; точность минутных тиков не подменяет persisted время начала | Conditional reservation + revision/attempt fence, история в DB transaction | Нет нового сервиса или пакета; при нескольких процессах требуется отдельная координация | Умеренная, повторное использование владельцев источников |
|
||||
|
||||
Выбран native timer с durable CAS: уже существующий `TbankSyncWork` и SQLite дают необходимые primitives, но сам таймер не является гарантом уникальности. Для истории использовать уже установленный Prisma/SQLite вместо time-series базы; данные малы, rollback и backup живут в существующем deployment. Для справочных сведений использовать действующих integration clients и online reads; новая cache/service boundary не оправдана, а долговременное кэширование прямо противоречит принятым требованиям. Отказ от него повышает зависимость detail от T-Bank, поэтому ошибки изолируются от сохранённых финансовых наборов. Перед настройкой фактического числа upstream вызовов требуется сверить действующие первичные ограничения провайдера и измерить размер/прирост локальной базы на реальном объёме; произвольные внешние числовые лимиты не фиксируются как допущение.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- При простое backend точки пропускаются; нет искусственной интерполяции и накопленной очереди RPC.
|
||||
- Один timer без отдельной межпроцессной аренды небезопасен для нескольких экземпляров: deployment сохраняет единственный process.
|
||||
- Переход на онлайн-справочник увеличивает latency и чувствительность detail/search к upstream; нельзя распространять ошибку на сохранённый портфель или его события.
|
||||
- Исчезновение/возврат счёта и одновременная смена token требуют fence внутри коротких транзакций; `deletedAt` без актуального подтверждения membership не даёт права чтения.
|
||||
- Удаление таблиц справочника до развязки FK/dependencies может стереть историю свечей; destructive cleanup откладывается до проверки сохранности рыночных данных.
|
||||
- Старый frontend и новый backend должны сосуществовать в переходе: публикация DTO статуса предшествует удалению screen POST; потребителю передаётся явный OpenAPI artifact.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Добавить историю и metadata последней попытки совместимым расширением SQLite; не переписывать уже применённые migrations и не выполнять reset на рабочей базе. Привязать запись истории к существующему portfolio publisher и видимость к подтверждённому membership/connection lifecycle.
|
||||
2. Ввести backend-only расписание поверх существующих source owners и read-only статус; включить первичную загрузку и восстановление после рестарта при сохранённом времени попытки. Только после проверки idempotency и отказов перевести GET на read-only для текущих личных данных.
|
||||
3. Развязать online reference от долговременных detail/search/lookup записей, сохранив минимальные identity и таблицы quotes/candles/forecast; сначала перевести readers, затем совместимой миграцией удалять лишь действительно неиспользуемые справочные payload, не каскадируя рыночную историю.
|
||||
4. Получить опубликованный backend OpenAPI и отдельно обновить frontend query/статус/кнопки в рамках согласованного contract. Затем убрать неиспользуемый screen-triggered endpoint; зафиксировать изменение ADR и as-built страниц. Измерить рост SQLite и provider call volume до изменения частоты/лимитера.
|
||||
|
||||
### Rollback Contract
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `required` |
|
||||
| Trigger | Нарушение доступа к истории после замены token, повторная публикация точек, порча сохранённых свечей при переходе справочника либо несовместимость нового frontend с API статуса. |
|
||||
| Actions | Остановить scheduler, вернуть предыдущие совместимые сборки из проверенного комплекта и восстановить DB из согласованного backup перед destructive переходом; при additive стадии допустимо вернуть старый runtime с сохранением новых неиспользуемых таблиц. Не возобновлять прежний screen-triggered режим одновременно с включённым hourly scheduler. |
|
||||
| Data safety | Сохранить исходные snapshots, coverage, forecast и точки истории; никогда не удалять или заново создавать production SQLite ради rollback. Перед очисткой справочника обеспечить проверенный backup/restore и доказать отсутствие каскадов в candles. |
|
||||
| Verification | Проверить на копии с представительными данными доступность прежних личных данных только у текущего подключения, идемпотентность точек, сохранность candles/coverage и совместимость опубликованного API до возврата трафика. |
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
Сейчас актуализация инвестиционных данных зависит от открытия экранов и ручного действия. Неактивный кабинет не получает новые данные, а долгосрочную динамику стоимости счёта восстановить по последнему снимку невозможно. Нужны предсказуемое часовое обновление независимо от frontend и накопление пригодных для будущего графика оценок. Согласованный объём — [issue #22](https://git.personal.ksv741.ru/WealthLedger/main/issues/22).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Обновлять доступные счета, состав и стоимость портфелей, актуальные операции и будущие события по бумагам портфеля раз в час силами backend; первое обновление выполнять после подключения. Сохранённые данные отдавать сразу, сохраняя последнее успешное состояние при ошибке и показывая результат последней попытки, включая частичный успех.
|
||||
- Накапливать оценку стоимости каждого счёта по валютам на фактическое время успешного получения полной оценки, без конвертации и подмены отсутствующей цены нулём. Сохранять историю при смене токена и отключении, скрывать её без подтверждённого доступа и продолжать при восстановлении доступа к тому же счёту.
|
||||
- Убрать ручную синхронизацию с T-Bank и обновление личных данных при открытии инвестиционных экранов. Открытые экраны перечитывают сохранённые данные каждые 30 минут. Сохранить первичную загрузку отсутствующих исторических интервалов и диапазонов свечей, но не перечитывать полностью загруженную старую историю автоматически.
|
||||
- Получать справочные данные инструмента и результаты поиска онлайн без отдельного долговременного хранения; сохранить подписи последнего снимка портфеля, события, котировки и свечи согласно их разным правилам актуализации.
|
||||
- Вне объёма: сам график и публичное чтение его точек, валютная конвертация, многопроцессное расписание, автоматическая повторная сверка уже полных старых интервалов операций и свечей, а также изменение действий для ручных банковских карт и повторного чтения после сетевой ошибки.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `specs/hourly-investment-sync/spec.md`: независимый от открытых экранов часовой обход и наблюдаемый результат последней попытки.
|
||||
- `specs/portfolio-value-history/spec.md`: точная история оценки по счёту и валюте с учётом полноты и границы доступа.
|
||||
- `specs/online-instrument-information/spec.md`: получение справочных сведений и поиска онлайн без сохранённой самостоятельной копии, отдельно от котировок, свечей и событий.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `specs/portfolio-view/spec.md`: чтение последнего снимка без запуска синхронизации, часовая свежесть, отсутствие ручной синхронизации и сохранение доступных подписей позиций.
|
||||
- `specs/brokerage-accounts/spec.md`: быстрое чтение сохранённых счетов и сводок, видимость частичных ошибок и периодическое перечитывание экрана.
|
||||
- `specs/tbank-connection/spec.md`: первая загрузка после подключения и сохранение скрытой истории стоимости при отключении/замене токена.
|
||||
- `specs/brokerage-account-upcoming-events/spec.md`: обновление прогноза по расписанию с сохранением последнего полного результата при сбое.
|
||||
- `specs/frontend-data-resilience/spec.md`: разделение повтора HTTP-чтения и запрещённой ручной синхронизации при часовом обновлении.
|
||||
- `specs/brokerage-account-ledger/spec.md`: первичная загрузка отсутствующего периода без повторной сверки уже полного исторического периода при открытии страницы.
|
||||
|
||||
## Impact
|
||||
|
||||
Пользователь быстрее открывает инвестиционные экраны и видит время и возможную неполноту последней синхронизации; открытый экран получает новые сохранённые данные без команды обновления. Затронуты инвестиционные экраны, фоновое обновление и хранение личных данных, правила доступа после смены подключения, отображение ошибок и онлайн-получение информации об инструментах. Переход требует согласовать действующие требования, которые пока предписывают короткие сроки свежести и ручные повторы.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Отсутствующий исторический интервал загружается только впервые
|
||||
|
||||
Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение недостающего интервала без ожидания T-Bank в локальном ответе. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы, часового обхода или удалённой команды ручной синхронизации. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным.
|
||||
|
||||
#### Scenario: Первый просмотр отсутствующего прошлого периода
|
||||
|
||||
- **WHEN** User открывает доступный исторический период без подтверждённой полноты
|
||||
- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных
|
||||
|
||||
#### Scenario: Повторное открытие полного периода
|
||||
|
||||
- **WHEN** User повторно открывает уже полностью загруженный прошлый период
|
||||
- **THEN** система читает сохранённые операции без повторной сверки с T-Bank
|
||||
|
||||
#### Scenario: Ошибка страницы загрузки
|
||||
|
||||
- **WHEN** загрузка части отсутствующего периода завершилась ошибкой
|
||||
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается как полная
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Upcoming events use complete source results
|
||||
Система MUST возвращать только последний полностью успешно сформированный прогноз по доступному счёту. Если необходимые данные брокера или MOEX недоступны во время часового обновления, система MUST NOT заменять последний успешный прогноз частичным списком или изменять время его успеха. Инструменты, для которых источник не предоставляет поддерживаемые события, MUST пропускаться без синтетических строк «нет данных». Получение сохранённого прогноза MUST NOT запускать синхронизацию при открытии экрана; ошибку последней попытки MUST показывать отдельно.
|
||||
|
||||
#### Scenario: Недоступность источника
|
||||
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен при часовом обновлении
|
||||
- **THEN** последний полный прогноз остаётся доступен с исходным временем, частичный список не публикуется, а ошибка отражается в статусе обхода
|
||||
|
||||
#### Scenario: Инструмент без поддерживаемых данных
|
||||
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
|
||||
- **THEN** система не добавляет для него синтетическую строку в результат
|
||||
|
||||
#### Scenario: MOEX отвечает без запрошенного блока
|
||||
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
|
||||
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
|
||||
|
||||
#### Scenario: Чтение при временном сбое обновления
|
||||
- **WHEN** User открывает события доступного счёта после неудачного часового обновления и для счёта существует прежний полный прогноз
|
||||
- **THEN** система возвращает прежний прогноз без ожидания источника и обозначает ошибку обновления отдельно
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Состояния недоступности списка счетов
|
||||
Система MUST читать подтверждённый доступный список счетов текущего подключения из последнего сохранённого результата и MUST NOT ожидать T-Bank или запускать проверку только из-за GET. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank уже отклонил сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection, не выдавая личных данных. Временная ошибка часового обновления MUST оставлять последний подтверждённый список доступным с исходным временем и отдельным состоянием ошибки; при ещё неизвестном списке система MUST обозначать ожидание первой загрузки, не выдавая частичный список за полный.
|
||||
|
||||
#### Scenario: Подключение отсутствует
|
||||
- **WHEN** User без сохранённого T-Bank connection запрашивает список
|
||||
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
|
||||
|
||||
#### Scenario: Сохранённый token больше не действителен
|
||||
- **WHEN** T-Bank отклоняет сохранённый token при часовом обновлении списка как неаутентифицированный или без прав
|
||||
- **THEN** последующее чтение получает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные, но connection сохраняется
|
||||
|
||||
#### Scenario: T-Bank временно недоступен
|
||||
- **WHEN** часовая проверка списка завершается временной либо иной upstream-ошибкой после ранее успешной проверки
|
||||
- **THEN** обычное чтение возвращает последний подтверждённый список без ожидания T-Bank, сохраняя его исходное время и отдельную ошибку обновления
|
||||
|
||||
#### Scenario: Первый обход ещё не завершён
|
||||
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
|
||||
- **THEN** система не выдаёт частичный или неподтверждённый список за полный и сообщает о продолжающейся загрузке
|
||||
|
||||
### Requirement: Экран списка брокерских счетов
|
||||
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручной синхронизации и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MAY перечитать локальный результат; пока экран открыт, он MUST перечитывать результат backend каждые 30 минут без запуска импорта. При уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового перечитывания и после его ошибки, ясно отделяя это состояние от успешного нового ответа. При успешном непустом ответе клиент 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** клиент получает успешный пустой массив
|
||||
- **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** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
|
||||
|
||||
### Requirement: Клиентские состояния ошибки списка счетов
|
||||
Клиент MUST NOT автоматически повторять неуспешный запрос списка вне согласованного периодического перечитывания открытого экрана. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного чтения. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового перечитывания, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. Эти действия MUST NOT инициировать синхронизацию с T-Bank.
|
||||
|
||||
#### Scenario: Подключение требуется
|
||||
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
|
||||
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||
|
||||
#### Scenario: Известная ошибка не повторяется автоматически
|
||||
- **WHEN** запрос списка завершается известной ошибкой
|
||||
- **THEN** клиент не выполняет немедленный повтор, пока User не выберет действие повторного чтения либо не наступит очередной 30-минутный срок открытого экрана
|
||||
|
||||
#### 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`
|
||||
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить чтение либо открыть настройки T-Bank
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
|
||||
Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для этого же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время перечитывания сохранённых данных и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повтор после транспортной ошибки MUST повторять чтение, а не инициировать синхронизацию с T-Bank.
|
||||
|
||||
#### 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 предупреждение независимо, не представляя один вид состояния другим
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
## Purpose
|
||||
|
||||
Задаёт независимое от открытых экранов часовое обновление инвестиционных данных пользователя и наблюдаемый результат последней попытки при частичных сбоях.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Часовой обход личных инвестиционных данных
|
||||
|
||||
Backend SHALL начинать первый обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки для подключения. При старте после простоя он MUST учитывать время последней попытки, выполнять не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Если попытки ещё не было и данных нет либо они старше часа, обход MUST начинаться при старте. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока даже после перезапуска. Перекрывающиеся обходы MUST NOT выполняться; если срок настал во время ещё идущего обхода, соответствующий запуск пропускается.
|
||||
|
||||
Обход MUST проверять доступные счета и обновлять портфель каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего портфеля. Он MUST NOT повторно загружать уже полные прошлые интервалы операций, завершённые свечи или полную рыночную историю. Он MUST NOT зависеть от открытого frontend. Смена подключения MUST NOT позволять завершившейся поздно попытке старого подключения изменять личные данные или время их успеха.
|
||||
|
||||
#### Scenario: Первое подключение между часовыми запусками
|
||||
|
||||
- **WHEN** пользователь успешно подключает T-Bank после запуска backend
|
||||
- **THEN** первый обход начинается без ожидания следующего календарного часа, а следующий допускается через час от начала первого
|
||||
|
||||
#### Scenario: Рестарт после неуспешной попытки
|
||||
|
||||
- **WHEN** backend перезапускается до истечения часа после неуспешного обхода с отсутствующими данными
|
||||
- **THEN** обход не повторяется при рестарте, а последнее успешное состояние остаётся доступным согласно действующей границе доступа
|
||||
|
||||
#### Scenario: Перекрытый часовой срок
|
||||
|
||||
- **WHEN** начатый час назад обход всё ещё выполняется
|
||||
- **THEN** второй обход не запускается, а пропущенная попытка не ставится в очередь сразу после завершения первой
|
||||
|
||||
#### Scenario: Возобновление после простоя
|
||||
|
||||
- **WHEN** backend не работал несколько часов и после старта обнаружил, что последняя попытка старше часа
|
||||
- **THEN** он запускает только один текущий обход без синтетических прошлых обновлений
|
||||
|
||||
### Requirement: Сохранность и статус последней попытки
|
||||
|
||||
Успех одного счёта или источника MUST сохраняться независимо от ошибки другого; неполный либо неуспешный результат источника MUST NOT заменять его последнее успешное состояние или время. Backend SHALL предоставлять аутентифицированному пользователю статус только его последнего обхода: время начала, состояние (выполняется, полностью успешен, частично успешен или неуспешен) и проблемные счета и разделы без чувствительных сведений. Кабинет MUST показывать результат, время и проблемные счета/разделы при частичном успехе; отсутствие состоявшейся попытки MUST быть отличимо от успеха. Статус не предоставляет возможности начать синхронизацию с frontend.
|
||||
|
||||
#### Scenario: Ошибка одного счёта
|
||||
|
||||
- **WHEN** портфели двух счетов обновились, а третий не обновился
|
||||
- **THEN** новые данные первых двух сохраняются, у третьего остаётся последнее успешное состояние, а статус обхода обозначает частичный успех и проблемный счёт
|
||||
|
||||
#### Scenario: Ошибка событий при успешных портфелях
|
||||
|
||||
- **WHEN** все портфели обновлены, но событие одной бумаги не обновилось
|
||||
- **THEN** последний успешный прогноз сохраняется и статус обхода обозначает частичный успех с проблемным разделом
|
||||
|
||||
#### Scenario: Пользователь другого подключения запрашивает статус
|
||||
|
||||
- **WHEN** авторизованный пользователь запрашивает статус чужого обхода либо попытки старого подключения
|
||||
- **THEN** система не раскрывает детали чужой или прежней границы доступа
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
## Purpose
|
||||
|
||||
Определяет получение справочных сведений и результатов поиска инструментов непосредственно от источника без самостоятельной долговременной копии, отдельно от личных данных и рыночной истории.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Справочные сведения и поиск запрашиваются онлайн
|
||||
|
||||
При чтении карточки акции или облигации и выполнении поиска система MUST получать справочные сведения инструмента, включая доступные название, тикер, ISIN, описание и показатели компании, онлайн, а не из отдельной долговременной сохранённой копии. Повторное чтение открытой страницы MUST запрашивать эти сведения снова. Недоступность источника MUST отображаться как ошибка соответствующего справочного блока, не скрывая доступные сохранённые позиции, операции, котировки, свечи и события. Подписи в последнем успешном снимке портфеля MAY использоваться как fallback для идентификации инструмента; они MUST NOT представляться как успешно полученные сейчас подробные сведения. Для расчёта будущих событий MAY запрашиваться необходимые справочные данные без их отдельного хранения.
|
||||
|
||||
#### Scenario: Повторное открытие карточки акции
|
||||
|
||||
- **WHEN** пользователь повторно открывает страницу акции после прежнего успешного чтения
|
||||
- **THEN** справочные сведения запрашиваются онлайн заново, а сохранённая копия карточки не выдаётся за актуальный ответ
|
||||
|
||||
#### Scenario: Источник справочника недоступен
|
||||
|
||||
- **WHEN** подробные сведения акции получить не удалось, но доступен последний сохранённый снимок портфеля
|
||||
- **THEN** справочный блок показывает локальную ошибку, а сохранённые позиции и их доступные подписи остаются видимы без обещания актуальности подробных сведений
|
||||
|
||||
### Requirement: Рыночные и событийные данные имеют отдельную политику
|
||||
|
||||
Система MUST продолжать сохранять котировки и запрошенные диапазоны свечей; при открытии страницы обновление текущей котировки и незавершённой свечи MUST выполняться не чаще одного раза в час. Будущие дивиденды, купоны и другие поддерживаемые события MUST сохраняться и обновляться в составе часового обхода бумаг портфеля. Полностью загруженные прошлые диапазоны свечей MUST NOT повторно загружаться автоматически. Получение этих данных MUST NOT запускать синхронизацию личных счетов при открытии страницы.
|
||||
|
||||
#### Scenario: Свежая котировка
|
||||
|
||||
- **WHEN** пользователь повторно открывает страницу инструмента менее чем через час после получения котировки
|
||||
- **THEN** система использует сохранённую котировку без преждевременного повторного запроса цены
|
||||
|
||||
#### Scenario: Исторический диапазон свечей открыт впервые
|
||||
|
||||
- **WHEN** пользователь открывает диапазон свечей, которого ещё нет локально
|
||||
- **THEN** система загружает его для просмотра, не начиная синхронизацию личного портфеля
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
## Purpose
|
||||
|
||||
Определяет накопление достоверных точек стоимости брокерского счёта по исходным валютам с сохранением истории между подключениями без раскрытия её вне подтверждённого доступа.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Полная оценка становится точкой истории
|
||||
|
||||
Система MUST добавлять точки на фактическое время каждого успешно полученного полного финансового снимка отдельного Brokerage Account, включая первую загрузку; стоимость MUST храниться раздельно по валютам без конвертации, полным JSON состава или подстановки искусственных точек за часы простоя. Если хотя бы у одной ненулевой позиции текущая стоимость не определена, точки MUST NOT записываться ни для одной валюты этого обновления, хотя новый состав счёта MAY оставаться текущим снимком. Пустой счёт без известной валюты MUST NOT создавать нулевую денежную точку. Ошибка получения обязательных данных MUST NOT менять предыдущие точки или последний успешный снимок. История MUST NOT иметь заранее установленного срока удаления.
|
||||
|
||||
#### Scenario: Полная оценка двух валют
|
||||
|
||||
- **WHEN** успешно получен полный снимок счёта со стоимостью в RUB и USD
|
||||
- **THEN** появляются отдельные точки RUB и USD с фактическим временем получения, без общего конвертированного значения
|
||||
|
||||
#### Scenario: Неизвестная цена одной позиции
|
||||
|
||||
- **WHEN** получен новый состав счёта с оценёнными RUB-активами и позицией без известной цены
|
||||
- **THEN** состав доступен как текущий снимок с неполной оценкой, но новые точки истории не появляются ни по одной валюте
|
||||
|
||||
#### Scenario: Пустой счёт без валюты
|
||||
|
||||
- **WHEN** успешно полученный счёт не имеет позиций, денег и известной валюты
|
||||
- **THEN** история не получает точку с придуманной валютой или суммой
|
||||
|
||||
### Requirement: История переживает смену подключения без потери приватности
|
||||
|
||||
История MUST продолжаться для того же User и идентификатора Brokerage Account после замены токена или повторного подключения, если доступ к счёту подтверждён текущим подключением. При отключении либо после успешно полученного списка без ранее доступного счёта история MUST оставаться сохранённой, но помечаться удалённой и скрываться от чтения; при повторном подтверждении того же счёта пометка MUST сниматься. Ошибка получения списка MUST NOT менять видимость истории. Подтверждённый отказ токена MUST скрывать личные данные до восстановления доступа; временный сбой MUST сохранять видимым последний успешный результат при действующей границе доступа. История другого пользователя MUST NOT раскрываться.
|
||||
|
||||
#### Scenario: Замена токена с прежним счётом
|
||||
|
||||
- **WHEN** новый проверенный токен пользователя даёт доступ к прежнему идентификатору счёта
|
||||
- **THEN** линия его истории продолжается, а поздняя работа старого токена не добавляет точек
|
||||
|
||||
#### Scenario: Счёт исчезает из подтверждённого списка
|
||||
|
||||
- **WHEN** успешно полученный полный список доступных счетов не содержит ранее доступный счёт
|
||||
- **THEN** его история сохраняется, помечается удалённой и не раскрывается до повторного подтверждения доступа
|
||||
|
||||
#### Scenario: Временный сбой списка
|
||||
|
||||
- **WHEN** обновление списка счетов завершилось временной ошибкой
|
||||
- **THEN** пометки удалённых счетов не меняются
|
||||
|
||||
#### Scenario: Отключение и повторное подключение
|
||||
|
||||
- **WHEN** пользователь отключил T-Bank, а позже подключил проверенный токен с тем же счётом
|
||||
- **THEN** до подтверждения доступа история скрыта, после подтверждения она снова видима и пополняется без разрыва идентичности счёта
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||
Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST читать сохранённые данные без ожидания T-Bank и MUST NOT запускать повторное обновление личного портфеля, даже если снимок отсутствует или старше часа; актуализацией владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обновления MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность либо свежесть финансового снимка. Сохранённые подписи позиций MAY оставаться доступны при недоступности онлайн-справочника.
|
||||
|
||||
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться в виде долговременного справочника; ошибка lookup не подтверждает отсутствие инструмента.
|
||||
|
||||
#### Scenario: Свежий Snapshot
|
||||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
|
||||
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Медленный lookup инструмента
|
||||
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
|
||||
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую в Snapshot доступную identity; подробные сведения остаются отдельным состоянием
|
||||
|
||||
#### Scenario: Fallback после временной upstream-ошибки
|
||||
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
|
||||
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
|
||||
|
||||
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio
|
||||
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
|
||||
|
||||
#### Scenario: Неполная оценка при новом составе
|
||||
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
|
||||
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции, без подстановки нуля
|
||||
|
||||
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||||
|
||||
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry или refetch при возврате фокуса. Открытый инвестиционный экран MUST перечитывать сохранённый результат каждые 30 минут без запуска синхронизации с T-Bank. Наблюдение известной активной работы MAY обновлять отображаемые ресурсы; чтение статуса и данных MUST NOT запускать импорт. Status сообщает состояние и ошибку без hash notifications и численных progress counters. После ухода со страницы наблюдение MUST прекращаться.
|
||||
|
||||
Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться. Новый диапазон свечей загружается при его открытии; initial loading допустим. При загрузке без ранее полученного успешного ответа для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
|
||||
|
||||
Истечение срока свежести MUST NOT само по себе скрывать уже доступный Client Cached Snapshot. Во время фонового обновления и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, включая валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST показывать компактную неблокирующую индикацию без замены данных skeleton. При ошибке перечитывания он MUST показывать локализованное неблокирующее предупреждение о последних полученных данных и предоставлять действие повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. Клиент MUST NOT предоставлять действие ручной синхронизации портфеля с T-Bank.
|
||||
|
||||
Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`, без нулевых валютных карточек. Для показываемого состава Portfolio клиент MUST сохранять время успешного получения снимка `lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST показывать Snapshot, его исходное время и нейтральное предупреждение об устаревших данных. Это предупреждение само по себе MUST NOT добавлять действие ручного обновления. Server-side stale и ошибка перечитывания Client Cached Snapshot MUST оставаться раздельными состояниями: ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения и действие повтора чтения MUST оставаться доступными при одновременном наличии этих состояний.
|
||||
|
||||
При отсутствии ранее успешно полученного Client Cached Snapshot этого же Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора чтения разрешено только для временной и неизвестной ошибки.
|
||||
|
||||
#### Scenario: Показ устаревшего Portfolio
|
||||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||
- **THEN** клиент показывает позиции, остатки, валютные итоги, время последнего успешного снимка и нейтральное предупреждение; само это предупреждение не добавляет действие ручного обновления
|
||||
|
||||
#### Scenario: Недоступный счёт на клиенте
|
||||
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный Client Cached Snapshot и клиент получает `404`
|
||||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
|
||||
|
||||
#### Scenario: Первичная загрузка Portfolio
|
||||
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа
|
||||
- **THEN** он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders
|
||||
|
||||
#### Scenario: Обновление после истечения срока свежести
|
||||
- **WHEN** срок свежести показанного Portfolio истёк и начался часовой обход данных того же Brokerage Account
|
||||
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного получения, показывает неблокирующую индикацию обновления и не заменяет доступный состав и сводку skeleton-состоянием
|
||||
|
||||
#### Scenario: Ошибка обновления после успешного ответа
|
||||
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а повторное HTTP-чтение этих же данных завершается ошибкой
|
||||
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка, показывает неблокирующее предупреждение и действие повтора чтения; ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
|
||||
|
||||
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание этого же Portfolio завершается ошибкой
|
||||
- **THEN** прежние суммы и время остаются видимыми одновременно с нейтральным server-side предупреждением, отдельным предупреждением об ошибке клиентского перечитывания и его действием повтора чтения
|
||||
|
||||
#### Scenario: Повторное обновление успешно
|
||||
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
|
||||
- **THEN** до ответа прежние суммы и время остаются видимыми, затем клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке клиентского чтения и показывает server-side предупреждение только в соответствии с новым `isStale`
|
||||
|
||||
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
|
||||
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Создание и замена подключения T-Bank
|
||||
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первая фоновая загрузка доступных личных данных без команды frontend. История стоимости прежних счетов MUST сохраняться, но показываться только для счетов, доступ к которым подтверждён новым подключением.
|
||||
|
||||
#### Scenario: Успешное создание подключения без счетов
|
||||
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
|
||||
- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }`; первая загрузка не создаёт фиктивные счета
|
||||
|
||||
#### Scenario: Успешная замена подключения
|
||||
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
|
||||
- **THEN** система заменяет token подключения, обновляет `connectedAt`, отвечает текущим статусом без token и начинает обновление подтверждённых счетов, не теряя их прежние исторические точки
|
||||
|
||||
#### Scenario: Невалидный token не заменяет существующее подключение
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
|
||||
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
|
||||
|
||||
#### Scenario: T-Bank временно недоступен при проверке token
|
||||
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
|
||||
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
|
||||
|
||||
### Requirement: Удаление подключения T-Bank
|
||||
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные текущие данные T-Bank. История стоимости счёта MUST сохраняться с пометкой удаления и MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
|
||||
|
||||
#### Scenario: Удаление существующего подключения
|
||||
- **WHEN** User удаляет существующее подключение
|
||||
- **THEN** система удаляет token и связанные кэшированные текущие данные User, скрывает сохранённую историю стоимости и отвечает `204`
|
||||
|
||||
#### Scenario: Повторное удаление подключения
|
||||
- **WHEN** User удаляет уже отсутствующее подключение
|
||||
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
|
||||
@@ -0,0 +1,68 @@
|
||||
## 1. Данные и граница доступа
|
||||
|
||||
- [x] 1.1 [backend] Добавить совместимое хранение точек оценки и видимости истории отдельно от revision; сохранять по валютам только полную оценку в той же транзакции, что и успешно заявленный снимок, с дедупликацией попытки.
|
||||
- **Covers:** `portfolio-value-history` — `Полная оценка становится точкой истории`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
|
||||
- **Verification:** Изолированная SQLite с применёнными прежними migrations: новая миграция сохраняет старые данные; повторная/поздняя попытка не создаёт лишних точек, неизвестная цена не записывает ни одной валюты, неуспешная публикация сохраняет снимок и историю.
|
||||
- [x] 1.2 [backend] Связать видимость истории с успешными PUT/DELETE подключения и публикацией полного membership; закрыть доступ старой revision и оставить историю для повторно подтверждённого accountId.
|
||||
- **Covers:** `portfolio-value-history` — `История переживает смену подключения без потери приватности`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank`
|
||||
- **Verification:** SQLite/HTTP проверки смены токена, отключения, отсутствующего и вернувшегося счёта, ошибки membership, чужого User и поздней попытки; прежние текущие datasets не раскрываются и история не исчезает.
|
||||
|
||||
## 2. Часовой обход и локальные чтения
|
||||
|
||||
- [x] 2.1 [backend] Применить выбранный в Design Solution Assessment: один native timer и durable conditional reservation для запуска и восстановления часовой попытки без параллельного обхода.
|
||||
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
|
||||
- **Verification:** SQLite/integration сценарии первого подключения, рестарта до и после 60 минут, пропущенных часов, перекрытого тика, замены revision и перезапуска незавершённой работы; ни один сценарий не создаёт конкурирующих попыток.
|
||||
- [x] 2.2 [backend] Оркестрировать подтверждённый membership, портфели счетов, изменяемый хвост операций и события через существующих владельцев источников; агрегировать результат последней попытки и открыть только read-only статус авторизованному User.
|
||||
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Сохранность и статус последней попытки`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
|
||||
- **Verification:** Integration/HTTP проверки: ошибка одного счёта или события даёт частичный статус с безопасным разделом, успешные источники публикуются, старый прогноз и время переживают сбой; чужой User/старая revision не видны, OpenAPI contract parity проходит.
|
||||
- [x] 2.3 [backend] Перевести обычные GET счетов, портфеля и прогноза на локальное чтение без запуска обновления личных источников; согласовать часовую свежесть, первый незавершённый обход и отдельно отображаемый сбой.
|
||||
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
|
||||
- **Verification:** HTTP/integration проверки локального ответа без RPC при missing/stale, неизвестного membership, временного сбоя, известного invalid token, неполной оценки и доступности последнего успешного снимка.
|
||||
- [x] 2.4 [backend] Оставить для журнала запуск только первичной загрузки отсутствующих интервалов; обновление изменяемого хвоста перенести в часовой обход, не перечитывая полные прошлые интервалы.
|
||||
- **Covers:** `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
|
||||
- **Verification:** SQLite/integration проверка пустого полного периода, неполной страницы, повторного открытия прошлого месяца, движения конца текущего периода и неповторного чтения сохранённой истории.
|
||||
|
||||
## 3. Инструменты и граница публичного API
|
||||
|
||||
- [x] 3.1 [backend] Развязать сохранённые котировки, свечи и события от справочных payload; обновлять текущую котировку и изменяемую свечу не чаще часа, сохраняя покрытия завершённых диапазонов и полные прогнозы.
|
||||
- **Covers:** `online-instrument-information` — `Рыночные и событийные данные имеют отдельную политику`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
|
||||
- **Verification:** Integration с изолированной SQLite: первая загрузка диапазона, отсутствие повторной загрузки полного прошлого диапазона, запрос свежей котировки, неудача онлайн-справочника и сохранность forecast/coverage/candles; миграция не удаляет связанные рыночные строки.
|
||||
- [x] 3.2 [backend] Перевести detail, company, поиск и lookup инструментов на онлайн-ответ через существующие integration clients; отделить их ошибку от сохранённых подписей и финансовых ресурсов без самостоятельной долговременной копии.
|
||||
- **Covers:** `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
|
||||
- **Verification:** HTTP/service проверки повторного онлайн-запроса, совпадения UID и ticker, пустого/неоднозначного результата и upstream-сбоя при видимом сохранённом портфеле; не создаются новые долговременные detail/search/negative lookup.
|
||||
- [x] 3.3 [backend] Сформировать публичный OpenAPI со статусом обхода, сохранив переходный контракт screen POST до готовности frontend.
|
||||
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
|
||||
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite для действующего и переходного wire contract; обычный GET не инициирует screen POST.
|
||||
- [x] 3.4 [backend] Проверить рост SQLite на представительном объёме и фактическую частоту upstream-вызовов; сопоставить её с действующей первичной документацией T-Bank перед настройкой частоты, выполнить штатные backend gates и durability для новой схемы.
|
||||
- **Covers:** `N/A` — операционная проверка выбранного в Design хранения и scheduler, без нового пользовательского поведения.
|
||||
- **Verification:** Выполнено: SQLite 617,263,104 bytes на synthetic profile (3 accounts, 300,000 operations, 200 instruments, 1,000,000 candles), persisted-read phase сделал 0 upstream calls; `npx prisma validate`, durability с pre-history baseline, `npm run test:e2e` и performance budget проходят. Ограничение `TBANK_RATE_LIMIT_PER_SECOND` (default 5 per backend instance) зафиксировано в `docs/adr/020-hourly-sync-verification-evidence.md`; production quota не расширяется.
|
||||
|
||||
## 4. Клиентские экраны
|
||||
|
||||
- [x] 4.1 [frontend] Получить опубликованный backend OpenAPI artifact и обновить локальный snapshot/generated types для статуса без импорта backend internals.
|
||||
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`
|
||||
- **Verification:** `npm run openapi:generate` и `npm run openapi:check` из frontend на предоставленном artifact; статус типизирован, отсутствие попытки и изоляция ошибки не заменены вымышленным успехом.
|
||||
- [x] 4.2 [frontend] Удалить инвестиционные кнопки/POST ручной синхронизации и source-recovery triggers; сохранить транспортный retry только как повтор чтения, а открытые инвестиционные экраны перечитывать раз в 30 минут.
|
||||
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`
|
||||
- **Verification:** UI/query проверки 30-минутного активного экрана, прекращения опроса после ухода, сохранения снимка при сбое и повторного HTTP GET без POST; auth-refresh и действия ручных карт не меняются.
|
||||
- [x] 4.3 [frontend] Показать в кабинете время/исход последнего обхода и названия проблемных счетов и разделов; справочный блок карточки отделить от позиций, событий, свечей и локальной ошибки чтения.
|
||||
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `brokerage-accounts` — `Экран списка брокерских счетов`
|
||||
- **Verification:** Проверки состояний `none/running/succeeded/partial/failed`, локального сбоя detail при видимых финансовых данных, доступных текстовых подписей и переноса статуса на узком экране согласно inline UI package Design revision 1.
|
||||
- [x] 4.4 [frontend] Завершить клиентский переход штатными gates и сверкой критического маршрута нового подключения → первый обход → статус → сохранённый портфель на открытом экране.
|
||||
- **Covers:** `N/A` — интеграционная проверка принятого UI package и публичного контракта после изменения нескольких экранов, без отдельного нового требования.
|
||||
- **Verification:** `npm run verify` в frontend и наблюдаемые состояния критического маршрута с предоставленным backend OpenAPI.
|
||||
|
||||
## 5. Завершение перехода
|
||||
|
||||
- [x] 5.1 [backend] После перехода frontend убрать неиспользуемый screen POST и наблюдение прежней работы без затрагивания нового статуса обхода.
|
||||
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
|
||||
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite: старый trigger недоступен, обычный GET и повтор чтения не запускают импорт.
|
||||
|
||||
## 6. Нормативная и as-built документация
|
||||
|
||||
- [x] 6.1 [specs] Проверить полное соответствие девяти Delta Specs одобренному Proposal и Design, сохранить точные имена Requirements и валидировать change и основные specs.
|
||||
- **Covers:** `N/A` — самостоятельная проверка нормативного артефакта и согласованной границы изменения, без добавления нового поведения.
|
||||
- **Verification:** `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate hourly-portfolio-sync-history --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, `npm run verify` в specs.
|
||||
- [x] 6.2 [docs] После проверки реализованного поведения дополнить глоссарий и объяснение свежести/истории, обновить соответствующие страницы инвестиций и зафиксировать новый ADR, который связывает изменённое решение с ADR 017, не переписывая исторический контекст.
|
||||
- **Covers:** `N/A` — as-built объяснение подтверждённых решений Design в отдельном docs owner, не параллельная норма требований.
|
||||
- **Verification:** Проверить маршруты чтения, локальные ссылки и смысл финансовых значений; `npm run verify` из docs.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd-v2
|
||||
created: 2026-10-09
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Completion Evidence: reconcile-hourly-investment-sync
|
||||
|
||||
## Outcome
|
||||
|
||||
The approved reconciliation was implemented across backend, frontend and docs.
|
||||
The hourly walk is the normal personal-data update owner; ordinary resource GETs
|
||||
and retries remain local/read-only, with the approved first-load exceptions.
|
||||
Resource metadata now distinguishes confirmed data state from hourly attempt state
|
||||
and browser transport state.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Result | Commit |
|
||||
| --- | --- | --- |
|
||||
| backend | Resource envelope, hourly attempt projections, saved forecast window semantics, read-only events GET, candle read boundary and generated OpenAPI | `aee330c` |
|
||||
| frontend | Generated contract consumer, Accounts/Portfolio/operations/events status presentation, HTTP-only retry, accessible status text and snapshot retention | `6b1b26a` |
|
||||
| docs | Six approved AS-IS documentation and ADR updates | `a45d3e7` |
|
||||
| specs | Approved v2 planning package; native sync/archive remains finalization work | pending archive commit |
|
||||
| main | N/A: no workspace/config/Compose orchestration change | N/A |
|
||||
| qa | N/A: separate QA repository does not exist | N/A |
|
||||
|
||||
## Verification
|
||||
|
||||
| Repository | Verification | Result |
|
||||
| --- | --- | --- |
|
||||
| backend | `npm run typecheck:app` | PASS |
|
||||
| backend | `npm run typecheck:test` | PASS |
|
||||
| backend | `npm test -- --runInBand` | PASS: 37 suites, 229 tests |
|
||||
| backend | `npm run test:sqlite -- upcoming-events.e2e-spec.ts` | PASS: 22/22 |
|
||||
| backend | `npm run openapi:generate` | PASS |
|
||||
| backend | `npm run test:openapi-contract` | PASS: 3 tests |
|
||||
| frontend | `npm run verify` | PASS: 65 files, 436 tests, typecheck, lint, OpenAPI, dependency gates, app and Storybook builds |
|
||||
| docs | `npm run verify` | PASS |
|
||||
| specs | `openspec validate reconcile-hourly-investment-sync --type change --strict --store wealth-ledger-specs` | PASS |
|
||||
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 26 specs, 0 failed |
|
||||
| all | `git diff --check` | PASS |
|
||||
|
||||
The backend repository-wide lint command reports existing unused ESLint
|
||||
suppressions. Changed-file lint with `--pass-on-unpruned-suppressions` passed;
|
||||
all application/test/type/OpenAPI gates above passed. This repository hygiene
|
||||
issue is not introduced by the reconciliation implementation.
|
||||
|
||||
## Independent Validation
|
||||
|
||||
Independent review found no remaining Critical or Important behavioral findings
|
||||
after fixing ordinary events GET locality, saved forecast window/calculatedAt,
|
||||
candle reread behavior and Accounts metadata presentation. The reviewed scope
|
||||
covered approved Proposal, ten Delta capabilities, System Analysis, Design, QA
|
||||
Analysis, Docs Impact, Tasks, current backend/frontend/docs diffs and generated
|
||||
OpenAPI.
|
||||
|
||||
## Approval Binding
|
||||
|
||||
Completion uses the current approved planning artifacts in this change directory.
|
||||
Finalization must re-run native sync/archive checks against the current files and
|
||||
must not treat artifact existence as approval. Legacy active changes remain on
|
||||
their legacy schema and are not implicitly migrated by this archive.
|
||||
@@ -0,0 +1,358 @@
|
||||
# Engineering Design
|
||||
|
||||
## Context and Constraints
|
||||
|
||||
The deployed backend is a single NestJS process with SQLite/Prisma. Its durable
|
||||
hourly walk, connection revision publication fence, saved investment resources,
|
||||
and frontend 30-minute rereads already provide most of the approved model.
|
||||
The remaining mismatch is at the resource boundary: backend
|
||||
`InvestmentResourceMetaDto` and `investmentResourceMeta` expose legacy
|
||||
`missing|partial|complete` availability and `idle|running|failed` work state,
|
||||
while the frontend projects that legacy metadata through `useResourceQuery` and
|
||||
uses `sync=observe` as a transport mode. The generated backend `openapi.json`
|
||||
and frontend snapshot agree with each other, but they describe this AS-IS
|
||||
contract rather than the approved target.
|
||||
|
||||
The backend-generated OpenAPI remains the executable contract. The change-local
|
||||
`system-analysis/contracts/investment-resource-envelope.schema.json` is a
|
||||
semantic target and must not become a second OpenAPI source, generated client,
|
||||
or runtime validator. Route identities, authentication/access boundaries,
|
||||
Problem Details codes, and resource payload DTOs remain established unless an
|
||||
approved target requirement explicitly changes them.
|
||||
|
||||
No normal personal resource GET may wait for or call T-Bank. The only
|
||||
acquisition exceptions remain the first unconfirmed ledger interval and first
|
||||
unconfirmed candle range. The design preserves the single-process boundary; it
|
||||
does not add distributed scheduling, a job service, polling endpoint, manual
|
||||
sync command, public value-history read, or a new UI package dependency.
|
||||
|
||||
## Technical Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
|
||||
- Publish one generated HTTP envelope for Accounts, Portfolio, operations, and
|
||||
events: `{ data, meta }`, with Accounts-only top-level `sync`.
|
||||
- Make resource-result state and hourly-attempt state independently truthful:
|
||||
confirmed data stays available after a failed/partial attempt, while an
|
||||
initial result remains `null` until complete.
|
||||
- Preserve local-read, connection-revision, and first-load invariants while
|
||||
migrating Swagger DTOs, mapper inputs, runtime responses, and frontend
|
||||
generated types together.
|
||||
- Keep the existing successful TanStack Query snapshot during read retries and
|
||||
retain the existing 30-minute open-screen reread cadence as a local HTTP
|
||||
reread, not an import trigger.
|
||||
|
||||
**Non-Goals**
|
||||
|
||||
- Rebuild source owners, hourly scheduling, portfolio-value storage, or the
|
||||
legacy status endpoint beyond removing their use as a source-work command
|
||||
from these screens.
|
||||
- Introduce a compatibility envelope, client-side DTO normalizer, duplicate
|
||||
cache, new route/version, manual refresh/import action, or a copy of OpenAPI
|
||||
in specs.
|
||||
- Plan documentation edits, implementation tasks, or product behavior beyond
|
||||
the approved proposal, delta specs, and System Analysis.
|
||||
|
||||
## Implementation Impact Map
|
||||
|
||||
### frontend
|
||||
|
||||
`affected` - generated transport types, `shared/api/resource-query.ts`, the
|
||||
existing brokerage-account/portfolio/operations/events query owners, and their
|
||||
page VMs/views consume the target metadata. `useScreenSync` remains the common
|
||||
30-minute open-screen reread owner, but its source-error derivation must read
|
||||
`meta.lastAttempt` rather than legacy `meta.sync`; retry stays HTTP-only.
|
||||
|
||||
### backend
|
||||
|
||||
`affected` - investment resource DTOs, mapper, endpoint response DTOs,
|
||||
resource readers, connection/hourly-attempt projections, Swagger annotations,
|
||||
and generated `openapi.json` implement the semantic envelope. Existing durable
|
||||
attempt, revision, membership, snapshot, forecast, coverage, and history
|
||||
owners remain the data owners.
|
||||
|
||||
### specs
|
||||
|
||||
`affected` - this Design makes the approved deltas and System Analysis
|
||||
implementable without duplicating the executable contract.
|
||||
|
||||
### docs
|
||||
|
||||
`affected` - durable AS-IS references are limited to ADR 019, ADR 017,
|
||||
`architecture/cross-cutting/data-freshness.md`,
|
||||
`domains/portfolio/flows/portfolio-loading.md`, and
|
||||
`domains/income/upcoming-events.md`. Their impact is recorded here only;
|
||||
documentation sequencing is not designed in this artifact.
|
||||
|
||||
### qa
|
||||
|
||||
`N/A` - no separate QA/E2E repository exists.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Backend: one resource-meta projection and one connection-walk projection
|
||||
|
||||
Replace the legacy shared metadata vocabulary at the existing
|
||||
`InvestmentResourceMetaDto`/`investmentResourceMeta` seam. Every affected
|
||||
resource envelope has required `meta.freshness`, `meta.completeness`,
|
||||
`meta.lastSuccessfulAt`, `meta.lastAttempt`, and `meta.availability`.
|
||||
`lastAttempt` is a resource-scoped projection with state
|
||||
`none|running|succeeded|partial|failed`, nullable start/end times, and only
|
||||
safe failed section/account/error-code entries. `none` is a concrete status,
|
||||
not an omitted field or success.
|
||||
|
||||
The mapper derives freshness and successful time only from the returned
|
||||
confirmed resource result. A later running, partial, or failed walk neither
|
||||
changes that time nor makes data fresh. `data: null` pairs with `unknown`/
|
||||
incomplete and either `initial-loading`, `temporarily-unavailable`, or
|
||||
`connection-unavailable`; a confirmed empty collection pairs with available
|
||||
and complete. The ledger's permitted first-load rows retain incomplete
|
||||
coverage and are never represented as a confirmed complete result.
|
||||
|
||||
Accounts additionally projects the connection-wide walk as `sync: null` when
|
||||
no attempt exists, otherwise the same complete safe attempt shape. Other
|
||||
resource envelopes do not gain top-level `sync`. This preserves the single
|
||||
shared metadata owner while avoiding the current ambiguity between per-resource
|
||||
`meta.sync` and Accounts-level walk status.
|
||||
|
||||
The existing `resourceKey`, `dataRevision`, `coverage`, and dependency details
|
||||
remain internal projection inputs or endpoint-specific metadata only where
|
||||
their approved behavior still requires publication; they must not substitute
|
||||
for any of the five required target meanings. No client must infer target state
|
||||
from their presence.
|
||||
|
||||
### Backend: retain storage owners and publication fences
|
||||
|
||||
The durable hourly attempt remains the sole normal publisher. Resource readers
|
||||
read the saved current connection revision and project its persisted result;
|
||||
they do not reserve work or contact upstream. Each successful section can
|
||||
persist independently, preserving the previous confirmed resource result and
|
||||
its success time when a later section fails. Forecast publication remains
|
||||
complete-source-only, and exact per-currency value-history writes remain in the
|
||||
same successful Portfolio publication transaction.
|
||||
|
||||
Connection PUT establishes a new revision before its first walk; DELETE and
|
||||
replacement prevent old personal snapshots and old attempt status from being
|
||||
read or published. Invalid-token knowledge blocks disclosure rather than being
|
||||
masked by saved data. Data and storage changes must be additive when a new
|
||||
persisted distinction is necessary; existing snapshots, coverage, forecasts,
|
||||
attempts, revisions, and retained value history are not reset or reclassified
|
||||
by a destructive migration.
|
||||
|
||||
### Contract: Swagger is produced at the DTO boundary
|
||||
|
||||
Swagger DTO annotations and endpoint response classes are the only place the
|
||||
wire schema is authored. The backend regenerates `openapi.json` from the
|
||||
implemented DTOs, then the frontend receives that published snapshot and runs
|
||||
its existing `openapi-typescript` generation. Hand-editing frontend
|
||||
`schema.d.ts`, adding handwritten parallel interfaces, or retaining a
|
||||
compatibility mapper would hide drift and is excluded.
|
||||
|
||||
The resulting generated contract must express nullable `data`, all required
|
||||
`meta` fields, nullable attempt timestamps, nullable problem fields, Accounts
|
||||
top-level nullable `sync`, and absence of top-level `sync` on the other
|
||||
resource responses. It must retain `sync=observe` only as a read-observation
|
||||
compatibility parameter while it exists in the generated route contract; it
|
||||
cannot cause source work and is not used to represent the new hourly state.
|
||||
The generated contract reconciles field spelling with the target schema before
|
||||
release, not through a second contract artifact.
|
||||
|
||||
### Frontend: one query cache and metadata-aware screen state
|
||||
|
||||
`useResourceQuery` remains the sole adapter that projects resource envelopes
|
||||
into `data`, `meta`, and Accounts `sync`, preserving its existing query key,
|
||||
authenticated transport, access-group invalidation, and Client Cached Snapshot
|
||||
behavior. It must project the generated target types directly. Resource query
|
||||
owners remain at their existing entity slices; page VMs, not presentation
|
||||
views, choose initial loading, unavailable/error, empty, and loaded states.
|
||||
|
||||
`useScreenSync` remains the single 30-minute timer for open investment
|
||||
screens. It calls ordinary local `refetch()` and its retry path continues to
|
||||
use the existing observation read without changing cache identity. It derives
|
||||
server attempt warning state from `meta.lastAttempt`, keeps transport failures
|
||||
separate, and never starts personal synchronization. Existing Accounts
|
||||
30-minute `staleTime` remains aligned with this cadence; Portfolio and other
|
||||
screen resources keep their current short cache freshness where already
|
||||
established while the open-screen timer supplies the approved reread rhythm.
|
||||
|
||||
All affected account screens render the five metadata meanings in text or an
|
||||
element with an accessible name, not color alone. A complete empty resource is
|
||||
normal empty content. `data: null` renders initial-loading or unavailable
|
||||
states without invented values. A failed/partial hourly attempt is an
|
||||
independent nonblocking warning beside confirmed data and its original time.
|
||||
`Portfolio.isStale` remains snapshot-specific and is shown separately from
|
||||
hourly-attempt and client HTTP-error warnings. A client snapshot stays visible
|
||||
during and after a failed background HTTP reread; connection replacement or
|
||||
deletion still invalidates personal queries.
|
||||
|
||||
## Cross-cutting Implementation
|
||||
|
||||
The canonical state split is:
|
||||
|
||||
| Concern | Owner | Observable representation |
|
||||
| --- | --- | --- |
|
||||
| Returned confirmed resource result | Saved resource owner | `data`, freshness, completeness, `lastSuccessfulAt`, availability |
|
||||
| Resource-relevant hourly outcome | Durable walk and resource projection | `meta.lastAttempt` |
|
||||
| Connection-wide hourly outcome | Durable walk | Accounts `sync` only |
|
||||
| In-flight/failed browser reread | TanStack Query | Existing query lifecycle and local retry warning |
|
||||
| Snapshot-specific Portfolio source age | Portfolio owner | `Portfolio.isStale` |
|
||||
|
||||
This split prevents a source failure from becoming a synchronous GET failure,
|
||||
prevents the UI from treating its cache as server fallback, and preserves safe
|
||||
access invalidation. Safe attempt problems contain only the approved section,
|
||||
optional account ID, and safe error code: no token, upstream payload, cursor,
|
||||
connection revision, or raw exception text is serialized or logged as status
|
||||
metadata.
|
||||
|
||||
Events retains its existing account and `all` scopes and its saved forecast
|
||||
window/filter semantics. Its metadata independently reports the latest events
|
||||
attempt, so a saved forecast can remain available when a later refresh fails.
|
||||
Operations retains its coverage semantics; only its explicitly approved missing
|
||||
historical first-load acquisition may work outside the hourly walk.
|
||||
|
||||
## Data / Storage Changes
|
||||
|
||||
No new storage architecture is needed. Existing durable tables/records for the
|
||||
current connection revision, hourly attempts, snapshots, operation coverage,
|
||||
forecast source windows, and retained portfolio-value points are authoritative.
|
||||
If implementation needs persisted terminal-state or problem-detail values not
|
||||
already durable, it extends the existing attempt record/schema additively and
|
||||
backfills conservatively: absent historical attempt information is projected as
|
||||
`none`, never as `succeeded`.
|
||||
|
||||
Migrations must preserve all current user, connection, personal-resource,
|
||||
coverage, forecast, and history rows. They must not reset SQLite, rewrite an
|
||||
old partial result as complete, expose retained history across a new connection
|
||||
boundary, or delete old data merely to fit the new vocabulary. Compatibility is
|
||||
achieved by deploying the backend DTO/runtime and its generated OpenAPI as one
|
||||
server contract, followed by the frontend snapshot/types and rendering change;
|
||||
there is no claim that an old frontend understands the target envelope.
|
||||
|
||||
## Contract Implementation
|
||||
|
||||
The implementation contract is the generated result of the following mapping:
|
||||
|
||||
| Target concept | Backend source | HTTP location |
|
||||
| --- | --- | --- |
|
||||
| Last confirmed result or no complete result | Existing local resource reader | `data` |
|
||||
| Result freshness and completeness | Resource mapper from confirmed result/coverage | `meta.freshness`, `meta.completeness` |
|
||||
| Original confirmed-result time | Saved source timestamp | `meta.lastSuccessfulAt` |
|
||||
| Relevant hourly result | Durable attempt plus resource section projection | `meta.lastAttempt` |
|
||||
| Whether complete data can be used | Connection/access/read result | `meta.availability` |
|
||||
| Complete current-connection walk | Durable connection-wide attempt | Accounts `sync` |
|
||||
|
||||
The target schema's `problemSection` shape is used for both scopes. For a
|
||||
connection-wide Accounts walk, its list may include all affected sections; for
|
||||
a resource-level attempt, it is restricted to failures relevant to that
|
||||
resource. A running attempt has `finishedAt: null`; terminal attempts have a
|
||||
completion time; `none` has null timestamps and no problems. These invariants
|
||||
are validated at runtime tests and in the generated OpenAPI, rather than by
|
||||
shipping the JSON Schema to application consumers.
|
||||
|
||||
## Developer Verification Strategy
|
||||
|
||||
Backend verification proves the contract at DTO, mapper, controller, and
|
||||
SQLite boundaries: successful empty data; first load with `data: null`; stale
|
||||
fallback after a failed walk; partial walk with preserved data; resource versus
|
||||
Accounts attempt scoping; events forecast-window filtering; invalid-token and
|
||||
connection-revision disclosure fences; no upstream personal call on ordinary
|
||||
GET; and both first-load exceptions staying narrow. It regenerates OpenAPI and
|
||||
runs the existing OpenAPI contract, type, lint, and relevant E2E suites.
|
||||
|
||||
Frontend verification updates the supplied snapshot, runs `openapi:check`, and
|
||||
uses MSW/page-VM tests to prove generated metadata projection, no automatic
|
||||
retry, HTTP-only retry, 30-minute open-screen reread, cache retention after a
|
||||
background error, connection invalidation, and distinct server stale/hourly
|
||||
attempt/client transport states. UI checks cover keyboard access, accessible
|
||||
status text, loading/error/empty/loaded states, and narrow viewport wrapping.
|
||||
|
||||
Before release, verify the backend-generated OpenAPI and frontend snapshot are
|
||||
the same published artifact, then run each repository's configured verification
|
||||
commands. In this specs store, run both v2 schema validation and strict change
|
||||
validation; structural success is supplemented by a semantic comparison against
|
||||
the approved deltas and System Analysis.
|
||||
|
||||
## UI/UX Package
|
||||
|
||||
**Applicability and scope:** `new/changed`. This is an inline package for the
|
||||
existing Accounts, account Portfolio, operations, and events screens. It adds
|
||||
the approved status vocabulary and preserves 30-minute local rereads; it does
|
||||
not introduce new composition, graphics, navigation, or controls.
|
||||
|
||||
**Package revision:** `1` (this Design). No visual files, external references,
|
||||
|
||||
**Screens, states and interactions:**
|
||||
|
||||
| Screen/state | Presentation | Interaction |
|
||||
| --- | --- | --- |
|
||||
| No complete result, initial attempt running | Existing structural loading state, labelled as loading | No manual sync action |
|
||||
| No complete result, attempt failed/unavailable | Safe unavailable/error state with target status text | Retry rereads HTTP only when recovery allows |
|
||||
| Complete empty result | Normal domain empty state and target status text | Existing navigation/actions only |
|
||||
| Complete data with running/succeeded/partial/failed attempt | Content and original success time remain visible; attempt appears as separate accessible status | No source-work action |
|
||||
| Client Cached Snapshot during/after HTTP failure | Existing content remains visible; transport warning remains distinct from server state | Existing retry rereads HTTP only |
|
||||
|
||||
**Responsive behavior:** reuse existing Mantine/theme components and page
|
||||
layouts. Status text wraps without hiding account content or money values on
|
||||
narrow screens; color supplements an accessible label. Existing skeletons
|
||||
remain noninteractive and unfocusable. Existing cards, buttons, and icons are
|
||||
used through their public package exports; no wrapper, SVG asset, token, or
|
||||
component package revision is introduced.
|
||||
|
||||
**Reused source and differences:** `N/A` for approved visual reuse. This is a
|
||||
new inline status package revision `1`, not an immutable reuse of a prior
|
||||
approved visual package. Existing cards, layouts, hooks, and UI packages are
|
||||
implementation inputs only; they are not the approved visual source. The
|
||||
package adds semantic presentation of the five target metadata fields and
|
||||
separate hourly-attempt/client-transport warnings. No local visual export is
|
||||
required.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Envelope evolution is a coordinated backend/frontend release risk. Generated
|
||||
artifacts prevent hidden type drift but cannot make an older client interpret
|
||||
new enum meanings.
|
||||
- Mapping resource relevance from connection-wide attempts can over-report or
|
||||
under-report a failure if it is not constrained by resource/section scope;
|
||||
mapper tests must cover each affected resource.
|
||||
- A durable `none` projection for old rows is intentionally less informative
|
||||
than inventing a historic success; it preserves truthfulness.
|
||||
- The 30-minute client timer can issue local reads while an hourly walk runs;
|
||||
this is acceptable because reads do not contend for upstream work and retain
|
||||
the last confirmed result.
|
||||
- Single-process scheduling remains unsuitable for multi-process deployment;
|
||||
adding leases, a worker, or distributed coordination requires a separate
|
||||
decision.
|
||||
- Documentation currently contains legacy request-driven/manual-refresh
|
||||
descriptions. Until the identified durable references are updated, they must
|
||||
not be used as implementation authority over the approved behavior.
|
||||
|
||||
## Migration / Rollout / Rollback
|
||||
|
||||
Rollout is backend-first at the contract source: apply only additive persistence
|
||||
changes if required, implement and test runtime projection/fences, regenerate
|
||||
and publish backend OpenAPI, then update the frontend snapshot/generated types
|
||||
and renderers. Do not enable the new projection alongside a second personal
|
||||
write owner. A frontend built from the old snapshot is not the compatibility
|
||||
target; rollout uses the matched generated artifact pair.
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `required` |
|
||||
| Trigger | Personal-data disclosure across connection revisions, false complete/fresh state, malformed generated contract, failure to preserve confirmed fallback, or a frontend unable to read the published contract. |
|
||||
| Actions | Stop the affected rollout, preserve a SQLite copy, return to the last compatible backend/frontend pair, and fix forward. Do not restore screen-triggered/manual synchronization while the hourly owner remains enabled. |
|
||||
| Data safety | Never reset production SQLite, delete additive attempt metadata, delete snapshots/coverage/forecasts/value history, or use a destructive downgrade. Old rows without target details safely project `none`. |
|
||||
| Verification | On a representative copy, verify migration integrity, revision/access fences, ordinary local GET behavior, preserved fallback timestamps, generated OpenAPI/snapshot identity, and frontend rendering before traffic resumes. |
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
| Option | Assessment |
|
||||
| --- | --- |
|
||||
| Evolve the existing shared DTO/mapper and generated OpenAPI | Chosen. It is the narrowest path: one resource-meta projection already owns all affected envelope metadata, Swagger already produces the contract, and frontend types already derive from the supplied snapshot. |
|
||||
| Add a target adapter/parallel endpoint or versioned envelope | Rejected. It duplicates mapping and cache paths, delays removal of ambiguous states, and creates an unnecessary second compatibility surface. |
|
||||
| Handwrite frontend target types or runtime validation | Rejected. Generated `paths`/schemas already own transport typing; manual copies would drift and a validator adds no trust boundary. |
|
||||
| Add a scheduler/queue/cache service to express status | Rejected. Durable single-process attempts and SQLite records already express the required state. A new service cannot improve this contract without changing the explicitly retained deployment model. |
|
||||
| Add a new design-system package/component family | Rejected. Existing Mantine and `@wealthledger/ui` states cover the textual, accessible status presentation; a new package would be abstraction without a new visual primitive. |
|
||||
|
||||
The chosen solution is intentionally a contract-and-projection reconciliation,
|
||||
not a new synchronization system. It minimizes code and migration surface while
|
||||
making the approved observable model expressible through the existing owners.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Documentation Impact
|
||||
|
||||
## Summary
|
||||
|
||||
Durable AS-IS documentation requires updates. The current pages still describe
|
||||
request-driven freshness, screen-started work, and legacy `sync` metadata;
|
||||
after delivery they must describe the hourly owner, local resource reads, and
|
||||
the independent resource-result and hourly-attempt state.
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|
||||
|---|---|---|---|---|---|
|
||||
| update | `architecture/cross-cutting/data-freshness.md` | cross-cutting | Ordinary personal-resource GETs and HTTP retries read saved local results and never start personal T-Bank work. The hourly walk is the normal update owner; only an unconfirmed historical ledger interval and an unopened candle range may acquire their own missing result. Document the independent `data` freshness, completeness, `lastSuccessfulAt`, availability, and resource-relevant last hourly attempt; retained complete data remains available after a later failed or partial attempt, while no complete result is `data: null`. Keep Client Cached Snapshot, `Portfolio.isStale`, and failed browser rereads distinct from server result and hourly-attempt state. | `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics; Design § Cross-cutting Implementation. | Compare the page with delivered resource responses and UI: normal GET/retry has no upstream personal call; documented state meanings match generated OpenAPI/runtime responses; stale fallback, no-result state, client read failure, and hourly failure remain separately explained. |
|
||||
| update | `domains/portfolio/flows/portfolio-loading.md` | domain | Portfolio loading reads the saved current-connection snapshot through a read-only envelope. The hourly walk refreshes Portfolio independently of the screen; the open screen's 30-minute reread and retry are local HTTP reads only. The page explains `data: null` before a complete snapshot, preserved snapshot/time after a failed or partial walk, separate resource metadata and `Portfolio.isStale`, and current-connection/revision disclosure fences without a manual refresh or status-observer flow. | `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; System Analysis §§ Contracts, Data and State Semantics, Error and Recovery Semantics; Design § Frontend: one query cache and metadata-aware screen state. | Read the delivered Portfolio route and screen against the page: GET and retry do not call T-Bank or expose a manual sync action; snapshot fallback retains its original time; no-data, empty, stale, failed hourly attempt, and failed HTTP reread are not conflated. |
|
||||
| update | `domains/income/upcoming-events.md` | domain | Upcoming events reads the saved forecast for account or `all` scope without starting personal sync. It documents strict inclusive UTC request dates, defaults, filtering to the intersection with the forecast's original inclusive window, and retention of that forecast's `calculatedAt` and source window. A later failed hourly events update leaves the last complete forecast readable and reports its failure independently; absent complete forecast is `data: null`, while an empty intersection is a successful empty result. | `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`; System Analysis § Contracts; System Analysis §§ Data and State Semantics, Error and Recovery Semantics; QA Analysis § Coverage Matrix: Events query and saved forecast. | Compare account and `all` responses/screens with the page: invalid range returns `INVALID_EVENTS_PERIOD` without source work; window intersection preserves original forecast metadata; empty intersection and failed-refresh fallback are correctly distinguished. |
|
||||
| update | `architecture/http-api.md` | architecture | The investment API section describes the generated backend OpenAPI as the executable contract for `{ data, meta }` resource responses: nullable `data`; required independent resource-result meanings; resource-scoped hourly `lastAttempt`; and Accounts-only connection-wide `sync`. It states that legacy `sync=observe` may remain a read-observation compatibility parameter but cannot start source work, removes the request-started screen-sync command/status model as current AS-IS, and retains established route identities and Problem Details behavior. | System Analysis §§ Contracts, Compatibility / Migration; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Contract: Swagger is produced at the DTO boundary; QA Analysis §§ Cross-system Verification, Regression Impact. | Compare prose and examples with the backend-generated OpenAPI and runtime responses: affected envelopes expose target nullability/enums and required metadata, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work. |
|
||||
| update | `adr/017-local-tbank-reads-and-background-sync.md` | ADR | Preserve ADR 017's historical decision and rationale, but mark its request-driven, manual-refresh, and no-periodic-walk decisions superseded by ADR 019. Retain its still-valid local-storage, completeness, and access-boundary context, with a link to current freshness documentation. | System Analysis § Durable Documentation Candidates; Proposal § Why; Design § Risks / Trade-offs. | ADR 017 explicitly identifies superseded decisions and links to ADR 019; it no longer presents a manual/request-driven sync model as current architecture. |
|
||||
| update | `adr/019-hourly-reads-and-online-instrument-reference.md` | ADR | Clarify the accepted hourly architecture: the last walk is scoped to the current connection; ordinary GET/retry does not acquire personal data; only the first unconfirmed ledger interval and first unopened candle range are narrow acquisition exceptions. Clarify that resource-result state and hourly-attempt state remain independent and that observation is not a command. | System Analysis §§ Interaction Flows, Contracts, Observability Semantics; `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Cross-cutting Implementation. | ADR 019 describes only the delivered hourly model and its bounded exceptions; it agrees with the updated freshness page and does not introduce a second HTTP contract source. |
|
||||
|
||||
## Diagram Promotion
|
||||
|
||||
None. `system-analysis/sequences/hourly-investment-walk.mmd` and
|
||||
`system-analysis/sequences/read-only-resource-get.mmd` are approval-time
|
||||
interaction evidence. Their durable operational meaning is covered by the
|
||||
updated freshness, portfolio-loading, events, and HTTP API pages; promoting
|
||||
them would duplicate those explanations without adding a durable navigation or
|
||||
architecture view.
|
||||
|
||||
## ADR Decision
|
||||
|
||||
N/A. No new architectural decision is required: ADR 019 already accepts the
|
||||
durable hourly-read architecture and is clarified above. ADR 017 is an
|
||||
historical decision whose conflicting request-driven/manual-refresh portions
|
||||
must be explicitly superseded, not rewritten into a new decision.
|
||||
|
||||
## Glossary / Navigation Impact
|
||||
|
||||
N/A. The change refines existing concepts (freshness, completeness, Client
|
||||
Cached Snapshot, Portfolio snapshot, and T-Bank connection) rather than adding
|
||||
a durable term needing a glossary definition or a new navigation entry.
|
||||
|
||||
## No-Change Rationale
|
||||
|
||||
No new documentation page, diagram, or machine-readable HTTP artifact is
|
||||
promoted. Backend-generated OpenAPI remains the executable HTTP contract; the
|
||||
change-local schema and sequences remain archived planning evidence. No docs
|
||||
entry is planned for private retained portfolio-value history because this
|
||||
change does not introduce a public history read, and its access boundary is
|
||||
already covered by the connection/freshness documentation.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Два завершённых legacy changes описывают несовместимые модели инвестиционной
|
||||
синхронизации. `persist-tbank-data` закрепляет запуск обновления запросом и
|
||||
ручное обновление, а `hourly-portfolio-sync-history` и фактическая система
|
||||
используют часовой обход и локальные read-only GET. Пока canonical specs и
|
||||
AS-IS документация содержат обе трактовки, ни один legacy change нельзя
|
||||
безопасно синхронизировать или архивировать.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Зафиксировать единственную observable model: часовой обход обновляет личные
|
||||
инвестиционные данные, а обычные GET читают сохранённый результат и не
|
||||
запускают личную синхронизацию.
|
||||
- Сохранить узкие исключения первой загрузки отсутствующего исторического
|
||||
интервала операций и впервые открытого диапазона свечей.
|
||||
- Уточнить видимые состояния свежести, полноты, последней попытки обхода и
|
||||
доступности личных данных после смены подключения.
|
||||
- Согласовать canonical behavior specs и durable AS-IS documentation с этой
|
||||
моделью, не создавая ещё один current OpenAPI source of truth.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Investment accounts, Portfolio, операции изменяемого периода, forecasts/events,
|
||||
personal connection boundary и связанные frontend recovery states.
|
||||
- Capabilities `brokerage-accounts`, `portfolio-view`,
|
||||
`brokerage-account-ledger`, `brokerage-account-upcoming-events`,
|
||||
`frontend-data-resilience`, `tbank-connection` и `tbank-data-sync`.
|
||||
- Описание и compatibility guidance для `{ data, meta }` resource responses и
|
||||
backend-generated OpenAPI как executable HTTP contract.
|
||||
- Обновление durable documentation и ADR supersession, когда это необходимо.
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
- Новый способ запуска синхронизации, публичное чтение истории стоимости,
|
||||
валютная конвертация и многопроцессный scheduler.
|
||||
- Переписывание historical legacy changes или автоматическое объявление их
|
||||
approval/Validation успешными.
|
||||
- Новая product feature, не требуемая для согласования уже поставленного
|
||||
поведения.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `hourly-investment-sync`: нормативно зафиксировать текущий наблюдаемый
|
||||
часовой обход и его статус, если capability ещё отсутствует в canonical specs.
|
||||
- `portfolio-value-history`: зафиксировать существующую историю оценок и
|
||||
границу её видимости, если capability ещё отсутствует в canonical specs.
|
||||
- `online-instrument-information`: зафиксировать online-only справочные
|
||||
сведения, если capability ещё отсутствует в canonical specs.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `brokerage-accounts`: локальное чтение и результат последнего обхода.
|
||||
- `portfolio-view`: read-only GET, часовая свежесть и отсутствие ручной
|
||||
личной синхронизации.
|
||||
- `brokerage-account-ledger`: первая загрузка отсутствующего интервала и
|
||||
локальное чтение полного прошлого периода.
|
||||
- `brokerage-account-upcoming-events`: последний complete forecast и
|
||||
независимое состояние ошибки обновления.
|
||||
- `frontend-data-resilience`: distinction between retrying an HTTP read and
|
||||
starting source work.
|
||||
- `tbank-connection`: границы текущих данных и скрытая история оценок после
|
||||
смены либо удаления подключения.
|
||||
|
||||
## Impact
|
||||
|
||||
Пользователь получает предсказуемое обновление инвестиций без скрытого запуска
|
||||
брокерских запросов при чтении экранов. Backend и frontend уже содержат большую
|
||||
часть целевого поведения; change формализует его, устраняет расхождения
|
||||
requirements/docs и создаёт основу для корректного завершения legacy history.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Canonical specs дают один непротиворечивый ответ о владельце обновления,
|
||||
исключениях первой загрузки и read-only GET.
|
||||
- Backend-generated OpenAPI/runtime interface семантически соответствует
|
||||
approved target contract при затронутых HTTP resource responses.
|
||||
- AS-IS docs и ADR guidance не обещают request-driven/manual sync там, где
|
||||
фактическая система использует hourly model.
|
||||
- Legacy `persist-tbank-data` и `hourly-portfolio-sync-history` получают
|
||||
безопасный дальнейший путь к archival finalization без конкурирующей sync.
|
||||
|
||||
## Assumptions / Dependencies
|
||||
|
||||
- Часовой scheduler, local resource reads и frontend 30-minute rereads остаются
|
||||
целевой моделью, подтверждённой Change Owner.
|
||||
- System Analysis определит точные contracts, semantics forecasts/events и
|
||||
допустимые first-load exceptions до implementation tasks.
|
||||
@@ -0,0 +1,92 @@
|
||||
# QA Analysis
|
||||
|
||||
## Verification Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Authorised, black-box verification of the affected personal-resource HTTP reads: Accounts, account and `all` Portfolio, operations, and account and `all` events.
|
||||
- The distinction between a local ordinary GET/retry and the permitted acquisition paths: the hourly walk, a first unconfirmed historical operations interval, and a first unopened candle range.
|
||||
- Observable hourly-walk scheduling and outcome semantics, including connection-scoped Accounts `sync` and resource `meta.lastAttempt`.
|
||||
- Resource-envelope semantics for confirmed data, empty confirmed data, first load, stale fallback, partial/failed attempts, and connection unavailability.
|
||||
- Current-connection access boundaries after T-Bank connection creation, replacement, and deletion, including retained value-history non-disclosure.
|
||||
- Events scope, date-window, saved-forecast, and failed-refresh behavior.
|
||||
- Compatibility of the backend-generated OpenAPI/runtime interface with the approved target envelope and preservation of established route identities and Problem Details behavior.
|
||||
- User-visible resilience: retry performs an HTTP reread only; cached successful screen data, server fallback, and failed background HTTP reads remain distinguishable.
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Scheduler implementation, persistence layout, internal job reservations, upstream-client mechanics, and frontend test structure.
|
||||
- New public reading of portfolio value-history, manual personal synchronization, currency conversion, multi-process scheduling, and changes to unrelated investment or banking resources.
|
||||
- Verifying T-Bank or public-reference-provider correctness beyond controlled responses needed to observe WealthLedger's published behavior.
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
| Approved behavior | Black-box verification | Expected observable result |
|
||||
| --- | --- | --- |
|
||||
| Ordinary personal GETs are read-only | Read Accounts, Portfolio, operations, and events repeatedly, including user retry and screen reopen, while upstream personal calls are observable. | Each read returns only locally held state and causes no personal upstream work or new hourly attempt. |
|
||||
| First-load exceptions stay narrow | Open an unconfirmed historical operations interval and, separately, an unopened candle range; then reread each after confirmation. | Only the requested missing interval/range may acquire data. It exposes incomplete local state until confirmed; no Accounts, Portfolio, events, other interval, or range refresh occurs. Confirmed reads stay local. |
|
||||
| Hourly ownership and cadence | Connect successfully; observe initial work, an elapsed eligible term, a restart after downtime, and a term arriving during an active walk. | Connection starts one initial attempt. Subsequent attempts begin no earlier than 60 minutes from the preceding start; restart performs at most one current attempt; overlapping terms neither run nor queue. |
|
||||
| Hourly result is observational | Observe no attempt, running, succeeded, partial, and failed results through affected resources and Accounts. | `meta.lastAttempt` distinguishes all five states with safe scoped problems; Accounts `sync` is `null` before an attempt and otherwise the connection-wide result. Reading it starts nothing. |
|
||||
| Independent section persistence | Make one account/section succeed and another fail during a walk. | Successful current data persists; the earlier complete result for the failed section remains visible; the attempt is `partial` with only safe section/account/error-code problem data. |
|
||||
| Envelope state meanings | Exercise a fresh complete result, stale complete fallback, no complete result while work runs, no complete result after failure, and confirmed empty Accounts/Portfolio/events. | Every success has `{ data, meta }`; `meta` separately reports freshness, completeness, `lastSuccessfulAt`, `lastAttempt`, and availability. `[]` is available and complete, while no complete result is `data: null` and never presented as complete or fresh. |
|
||||
| Failed refresh preserves known data | Cause a temporary hourly failure after a confirmed Accounts, Portfolio, operations, or events result. | `200` retains the original complete data and success time; freshness reflects that result; the failed attempt is separately visible. A completed background failure is not a synchronous `503`. |
|
||||
| Invalid connection boundary | Request personal resources with no connection, then after a known-invalid token result. | No connection returns established `409 TBANK_NOT_CONNECTED`; invalid token returns established `400 INVALID_TBANK_TOKEN`, retains the connection record, and discloses no personal fallback data. |
|
||||
| Connection replacement and deletion | Replace a valid connection while an old walk is pending or running, then delete the replacement twice. | PUT validates before changing the prior connection, establishes a new boundary and initial walk on success; old data/status cannot reappear. DELETE returns `204` idempotently, stops use of its token, and hides current personal data and retained history until the same account is confirmed again. |
|
||||
| Events query and saved forecast | Read account and `all` events for default, explicit, intersecting, non-intersecting, and empty windows; submit `from > to`; fail a later refresh. | Dates are strict inclusive UTC dates; defaults are current UTC date through +30 calendar days; invalid range returns `400 INVALID_EVENTS_PERIOD` without source work. Results are the intersection with the forecast's original window and retain its `calculatedAt`/window. Empty intersections are successful empty data. `all` is complete only with every included account contribution. Failed refresh retains the previous forecast and reports the attempt failure separately. |
|
||||
| Frontend recovery and accessibility | Observe a successful resource screen during HTTP reread failure, a server stale/fallback result, a failed hourly attempt, and each no-data first-load state. | Successful cached screen data remains visible during/after background HTTP failure. Retry only rereads HTTP. Client-read failure, server stale data, and failed hourly work are distinct, and the five `meta` meanings are exposed in text or accessible names rather than color alone. |
|
||||
|
||||
## Cross-system Verification
|
||||
|
||||
1. Use a controlled upstream boundary to prove that normal GETs and retry actions do not cause personal upstream requests, while an eligible hourly walk and each permitted first-load exception have only their approved source effect.
|
||||
2. Verify the API response and the rendered Accounts, Portfolio, operations, and events states together for the same server conditions. In particular, a saved complete result plus a failed attempt must remain readable and visibly distinguishable from a client HTTP-read failure.
|
||||
3. Verify that successful PUT replacement and DELETE invalidate visible personal-resource state. Late responses or late old-connection work must not restore accounts, Portfolio, events, `sync`, or retained history across the new boundary.
|
||||
4. Compare the generated backend OpenAPI published for delivery with runtime responses and the consumer-generated types. Confirm the target `{ data, meta }` resources, Accounts-only `sync`, all required meta meanings, nullability, and enum values are represented without relying on legacy `sync=observe` to start work.
|
||||
|
||||
## Risk-based Priorities
|
||||
|
||||
| Priority | Risk | Verification focus |
|
||||
| --- | --- | --- |
|
||||
| P0 | A screen read initiates brokerage work or leaks data across a connection boundary. | Upstream-call absence on GET/retry; replacement/deletion fence; invalid-token and no-connection responses. |
|
||||
| P0 | A failed refresh hides or mislabels financial data. | Complete fallback retention; `data: null` when no complete result exists; empty complete results; independent meta and attempt states. |
|
||||
| P0 | Events return an incorrect period, silently omit an account in `all`, or relabel an old forecast. | Strict UTC window validation/defaults/intersection; `all` completeness; preserved `calculatedAt` and original forecast window after failure. |
|
||||
| P1 | Scheduler work overlaps, catches up, or reports unsafe/incorrect status. | First walk, 60-minute eligibility from start, downtime, overlap drop, partial result, safe problem scope. |
|
||||
| P1 | Contract evolution breaks the generated frontend consumer or existing API callers. | Generated OpenAPI/runtime semantic comparison, route and established Problem Details compatibility, legacy observation endpoint non-command behavior. |
|
||||
| P2 | Presentation obscures recovery and freshness meaning. | Accessible status text, separate client/server/hourly indicators, retained screen snapshot, empty states. |
|
||||
|
||||
## Test Data and Environment Needs
|
||||
|
||||
- An authenticated user with controlled valid, invalid, and unavailable T-Bank outcomes; at least two supported accounts plus an unsupported account to establish membership and partial-result cases.
|
||||
- Persisted fixtures for fresh and stale complete Accounts, Portfolio, operations, and forecasts; no-result first-load state; confirmed empty Accounts; empty Portfolio; empty event intersection; and a forecast whose source window differs from the request window.
|
||||
- Controllable clock and observable upstream boundary to establish hourly eligibility, running overlap, restart behavior, and proof that normal reads have no personal upstream activity.
|
||||
- Two valid connection identities with disjoint accounts and one shared account, plus retained value-history data, to verify replacement/deletion concealment and re-confirmed access.
|
||||
- Published backend OpenAPI for the delivered build and the corresponding generated frontend contract, both from the same build candidate.
|
||||
|
||||
## Manual / Exploratory Scope
|
||||
|
||||
- Navigate Accounts, account Portfolio, operations, and upcoming events through first load, available empty, stale fallback, failed hourly update, invalid token, and no-connection states. For operations, include an incomplete first-loaded interval and its later confirmed result. Confirm status meaning is understandable without color and no manual personal-sync control is exposed.
|
||||
- Keep an investment screen open across its periodic reread and induce a failed HTTP read. Confirm the prior screen snapshot remains visible and that retry changes only the HTTP-read state.
|
||||
- Replace or delete a connection while navigating between account and event views. Confirm neither navigation, browser history, nor a late response exposes data from the prior connection.
|
||||
- Explore boundary dates around UTC midnight, the inclusive forecast window endpoints, leap-day dates, and empty intersections for both account and `all` events scopes.
|
||||
|
||||
## Automation Candidates
|
||||
|
||||
- Repeatable system checks for the resource-envelope state matrix, hourly cadence/overlap, local-read upstream-call absence, connection revision fence, and event window/scope matrix.
|
||||
- Consumer-contract compatibility checks that compare the generated backend OpenAPI, runtime resource responses, and regenerated frontend transport types for affected public operations.
|
||||
- Browser-level checks for retry-only reads, retained successful UI data, invalidation after connection change, and accessible status names.
|
||||
|
||||
## Observability Verification
|
||||
|
||||
- Confirm resource responses expose no attempt as `none`, running work without `finishedAt`, and terminal succeeded/partial/failed work with only safe section, account, and error-code problem details.
|
||||
- Confirm `lastSuccessfulAt`, freshness, completeness, and availability continue to describe the returned data rather than the later attempt; `sync` is `null` before any Accounts walk and remains connection-scoped thereafter.
|
||||
- Confirm no response, OpenAPI example, UI status, or observable problem detail discloses a token, upstream payload, cursor, internal connection revision, or old-connection status.
|
||||
|
||||
## Regression Impact
|
||||
|
||||
- Existing clients of the affected GET routes must retain route identities, account and `all` scopes, date validation/defaults, response ordering where specified, and established Problem Details codes unless the approved generated contract explicitly changes them.
|
||||
- Existing `sync=observe` and `/api/v1/tbank-sync/{id}` observation behavior may remain compatible but must not regain request-started personal synchronization semantics.
|
||||
- Accounts, Portfolio, operations, and events callers must handle the approved envelope evolution, including nullable `data`, independent attempt state, empty complete data, and Accounts-only `sync`; generated frontend types must match the published backend OpenAPI.
|
||||
- Unaffected public history-reading capability is not introduced. Retained value history remains unavailable outside a confirmed current connection boundary.
|
||||
|
||||
## Gaps / Blockers
|
||||
|
||||
None identified in the approved System Analysis and deltas. Field names used by the generated OpenAPI remain a delivery compatibility check: they must faithfully represent the approved target semantics before release.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Отсутствующий исторический интервал загружается только впервые
|
||||
Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение только недостающего интервала без ожидания T-Bank в локальном ответе. Это является исключением первой загрузки и MUST NOT запускать обновление других личных данных. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы или часового обхода. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным.
|
||||
|
||||
#### Scenario: Первый просмотр отсутствующего прошлого периода
|
||||
- **WHEN** User открывает доступный исторический период без подтверждённой полноты
|
||||
- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных
|
||||
|
||||
#### Scenario: Повторное открытие полного периода
|
||||
- **WHEN** User повторно открывает уже полностью загруженный прошлый период
|
||||
- **THEN** система читает сохранённые операции без повторной сверки с T-Bank
|
||||
|
||||
#### Scenario: Ошибка загрузки
|
||||
- **WHEN** загрузка части отсутствующего периода завершается ошибкой
|
||||
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: User can retrieve upcoming events for an account
|
||||
Система MUST предоставлять аутентифицированному User read-only `GET /api/v1/brokerage-account-upcoming-events` для выбранного открытого поддерживаемого 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`, не подменять период, не запускать загрузку другого периода и не возвращать его прогноз. Обычный GET MUST NOT запускать личную синхронизацию.
|
||||
|
||||
Ответ MUST иметь `{ data, meta }`, где `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Доступный `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, `calculatedAt`, а в режиме `all` -- принадлежность каждого события счёту. Прогноз MUST быть рассчитан по количеству бумаг полного Portfolio Snapshot на `calculatedAt`; сохранённый прогноз MUST сохранять свой исходный `calculatedAt` и рассчитанное от него включительное окно, даже когда запрошенное `from`/`to` отличается. При чтении сохранённого прогноза система MUST возвращать только события, чьи даты попадают во включительное пересечение исходного окна прогноза и запрошенного UTC-окна; пустое пересечение либо отсутствие событий в пересечении MUST возвращать успешный пустой список с теми же `calculatedAt`, `meta` и исходным окном. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта; отсутствие вклада счёта MUST NOT подменяться полным результатом. Список будущих событий является прогнозом по наблюдаемому состоянию счёта и количеству бумаг на `calculatedAt` и MUST NOT означать долговременное право владения Brokerage Account независимо от T-Bank connection.
|
||||
|
||||
#### Scenario: Получение событий доступного счёта
|
||||
- **WHEN** аутентифицированный User запрашивает события доступного счёта с последним полным прогнозом
|
||||
- **THEN** система возвращает из сохранённого прогноза события в запрошенном включительном UTC-окне, его исходные `calculatedAt` и рассчитанное окно без обращения к T-Bank
|
||||
|
||||
#### Scenario: Запрошенное окно не пересекается с сохранённым прогнозом
|
||||
- **WHEN** запрошенный включительный UTC-окно не пересекается с исходным окном последнего полного прогноза
|
||||
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`
|
||||
|
||||
#### Scenario: Отсутствие событий в периоде
|
||||
- **WHEN** пересечение запрошенного и исходного окна прогноза не содержит событий
|
||||
- **THEN** система возвращает успешный пустой список, момент расчёта, исходное окно и `meta`
|
||||
|
||||
#### Scenario: Некорректный период
|
||||
- **WHEN** User передаёт `from` позже `to`
|
||||
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD` без прогноза и без запуска личной синхронизации
|
||||
|
||||
#### Scenario: Прогноз всех счетов
|
||||
- **WHEN** User запрашивает режим `all` и для каждого входящего счёта доступен полный прогноз
|
||||
- **THEN** система возвращает события всех счетов в запрошенном окне с принадлежностью каждого события счёту
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
- **WHEN** User запрашивает события счёта, недоступного через его текущий T-Bank connection
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
|
||||
|
||||
### Requirement: Forecasts preserve event meaning and uncertainty
|
||||
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного Portfolio Snapshot на `calculatedAt`, использованного для сохранённого прогноза, а не по количеству бумаг на момент HTTP-запроса. Сохранённый прогноз MUST сохранять свой `calculatedAt` и эту исходную зависимость, в том числе после неуспешного обновления, и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
|
||||
|
||||
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
|
||||
|
||||
#### Scenario: Прогноз купона по текущей позиции
|
||||
- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза
|
||||
- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot
|
||||
|
||||
#### Scenario: Прогноз дивиденда
|
||||
- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза
|
||||
- **THEN** система возвращает сохранённое событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на количество акций этого Snapshot
|
||||
|
||||
#### Scenario: Отменённый дивиденд не попадает в прогноз
|
||||
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
|
||||
- **THEN** система не добавляет такое событие в сохранённый прогноз
|
||||
|
||||
#### Scenario: Отображение оферты без суммы
|
||||
- **WHEN** для поддерживаемой облигации известна оферта в пределах исходного окна прогноза, но её денежные условия недоступны
|
||||
- **THEN** система возвращает событие offer с прогнозной суммой `null`
|
||||
|
||||
#### Scenario: Неподдерживаемое частичное погашение
|
||||
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
|
||||
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
|
||||
|
||||
### Requirement: Upcoming events use complete source results
|
||||
Система MUST публиковать прогноз только после полного получения необходимых данных брокера и MOEX для полного Portfolio Snapshot, по количеству бумаг которого он рассчитан. Неуспешное обновление MUST NOT заменять последний полный прогноз, его `calculatedAt` или окно событий частичным списком. При неуспешном обновлении и существующем последнем полном прогнозе система MUST возвращать отфильтрованную для запрошенного окна часть этого прогноза с его исходными `calculatedAt`, окном и `meta`, отдельно обозначающим неуспех последней попытки, даже если результат фильтрации пуст. При отсутствии последнего полного прогноза система MUST возвращать `data: null` и в `meta` обозначать выполнение первой загрузки либо её неуспех без частичного списка. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
|
||||
|
||||
#### Scenario: Недоступность источника
|
||||
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен во время часового обновления
|
||||
- **THEN** система не публикует частичный список как полный прогноз
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе
|
||||
- **WHEN** часовой обход не завершает обновление необходимых источников, а последний полный прогноз существует
|
||||
- **THEN** система возвращает его события только в запрошенном окне, исходные `calculatedAt` и окно, а `meta` отдельно обозначает ошибку обновления
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе вне запрошенного окна
|
||||
- **WHEN** часовой обход не завершает обновление, последний полный прогноз существует, но его окно не пересекается с запрошенным
|
||||
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`, обозначающим ошибку обновления
|
||||
|
||||
#### Scenario: Инструмент без поддерживаемых данных
|
||||
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
|
||||
- **THEN** система не добавляет для него синтетическую строку в результат
|
||||
|
||||
#### Scenario: MOEX отвечает без запрошенного блока
|
||||
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных
|
||||
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
|
||||
|
||||
### Requirement: Account page presents an upcoming events list
|
||||
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
|
||||
|
||||
#### Scenario: Просмотр ближайших событий
|
||||
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
|
||||
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы
|
||||
|
||||
#### Scenario: Пустое состояние страницы
|
||||
- **WHEN** запрос будущих событий успешно возвращает пустой список
|
||||
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
|
||||
|
||||
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
|
||||
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
|
||||
|
||||
#### Scenario: Ошибка страницы
|
||||
- **WHEN** запрос будущих событий завершается ошибкой
|
||||
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
|
||||
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
|
||||
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
|
||||
|
||||
### Requirement: Account overview includes upcoming events
|
||||
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
|
||||
|
||||
#### Scenario: Отображение ближайших событий
|
||||
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
|
||||
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице
|
||||
|
||||
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
|
||||
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
|
||||
|
||||
#### Scenario: Ближайшие события недоступны
|
||||
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
|
||||
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
|
||||
|
||||
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
|
||||
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
|
||||
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Список активных брокерских счетов
|
||||
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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.
|
||||
|
||||
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
|
||||
- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
|
||||
- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank
|
||||
|
||||
#### Scenario: Пустой список поддерживаемых счетов
|
||||
- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов
|
||||
- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат
|
||||
|
||||
#### Scenario: Пустое название получает fallback
|
||||
- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя
|
||||
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
|
||||
|
||||
#### Scenario: Дата открытия отсутствует
|
||||
- **WHEN** последний подтверждённый счёт не имеет даты открытия
|
||||
- **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** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков
|
||||
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||
|
||||
### Requirement: Состояния недоступности списка счетов
|
||||
Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный.
|
||||
|
||||
#### Scenario: Подключение отсутствует
|
||||
- **WHEN** User без сохранённого T-Bank connection запрашивает список
|
||||
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
|
||||
|
||||
#### Scenario: Сохранённый token больше не действителен
|
||||
- **WHEN** T-Bank отклоняет сохранённый token при часовом обходе списка как неаутентифицированный или без прав
|
||||
- **THEN** последующее чтение отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные
|
||||
|
||||
#### Scenario: Ошибка обхода после успешного списка
|
||||
- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки
|
||||
- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank
|
||||
|
||||
#### Scenario: T-Bank временно недоступен
|
||||
- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой
|
||||
- **THEN** система сообщает о неуспешной первой загрузке без счетов
|
||||
|
||||
#### Scenario: Первый обход ещё не завершён
|
||||
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
|
||||
- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка
|
||||
|
||||
### Requirement: Экран списка брокерских счетов
|
||||
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Он MUST использовать успешный Client Cached Snapshot списка в течение 30 минут. Открытый экран Accounts MUST перечитывать локальный backend-результат каждые 30 минут и MUST NOT запускать импорт T-Bank. Клиент MUST показывать наблюдаемые freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. При уже имеющемся успешном списке клиент MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового HTTP-перечитывания и после его ошибки, ясно отделяя это состояние от свежего backend-результата. При успешном непустом ответе клиент 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** клиент получает успешный пустой массив
|
||||
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
|
||||
|
||||
#### Scenario: Последняя попытка неуспешна после полного списка
|
||||
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
|
||||
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке
|
||||
|
||||
#### Scenario: Первая загрузка списка выполняется или неуспешна
|
||||
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного
|
||||
|
||||
#### Scenario: Список берётся из свежего frontend-кэша
|
||||
- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки
|
||||
- **THEN** клиент показывает сохранённые счета без нового API-запроса
|
||||
|
||||
#### Scenario: Устаревший список загружается повторно
|
||||
- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список
|
||||
- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление
|
||||
|
||||
#### Scenario: Открытый экран перечитывает список
|
||||
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
|
||||
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
|
||||
|
||||
#### Scenario: Ошибка фонового обновления не скрывает snapshot
|
||||
- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
|
||||
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки
|
||||
|
||||
#### Scenario: Карточка ограничивает валютные итоги
|
||||
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
|
||||
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
|
||||
|
||||
#### Scenario: Недоступность сводки локальна карточке
|
||||
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
|
||||
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
|
||||
|
||||
#### Scenario: Пустая сводка не является ошибкой
|
||||
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
|
||||
|
||||
### Requirement: Клиентские состояния ошибки списка счетов
|
||||
Клиент MUST NOT автоматически повторять неуспешное HTTP-чтение списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного HTTP-чтения. Повтор MUST перечитывать backend-результат и MUST NOT запускать импорт T-Bank. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия.
|
||||
|
||||
#### Scenario: Подключение требуется
|
||||
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
|
||||
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||
|
||||
#### Scenario: Известная ошибка не повторяется автоматически
|
||||
- **WHEN** HTTP-чтение списка завершается известной ошибкой
|
||||
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения
|
||||
|
||||
#### Scenario: Token требует внимания
|
||||
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
|
||||
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||
|
||||
#### Scenario: Временная ошибка T-Bank
|
||||
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
|
||||
- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
|
||||
|
||||
#### Scenario: Неизвестная ошибка загрузки
|
||||
- **WHEN** клиент получает ошибку без известного кода списка счетов
|
||||
- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение
|
||||
|
||||
#### Scenario: Известная ошибка при сохранённом snapshot
|
||||
- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
|
||||
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
|
||||
Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для того же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Для каждого личного resource response клиент MUST представлять freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` текстом либо доступно именованным элементом, а не только цветом; он MUST различать отсутствие попытки, выполнение, полный успех, частичный успех, неуспех, доступный пустой полный результат и отсутствие полного результата. Client Cached Snapshot MUST оставаться видимым во время фонового HTTP-перечитывания и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повторное действие User MUST повторять только HTTP-чтение и MUST NOT запускать личную синхронизацию. Открытый инвестиционный экран MAY периодически перечитывать backend-результат, но MUST NOT создавать импорт T-Bank.
|
||||
|
||||
#### Scenario: Список Brokerage Accounts обновляется в фоне
|
||||
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое HTTP-перечитывание того же списка
|
||||
- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор перечитывания
|
||||
|
||||
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
|
||||
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее HTTP-чтение завершается ошибкой
|
||||
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение и предоставляет повтор только HTTP-чтения
|
||||
|
||||
#### Scenario: Server-side fallback и client cached snapshot различаются
|
||||
- **WHEN** backend возвращает Portfolio с `isStale` либо HTTP-перечитывание Client Cached Snapshot завершается ошибкой
|
||||
- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя одно состояние другим
|
||||
|
||||
#### Scenario: Resource сохраняется после неуспешной часовой попытки
|
||||
- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки
|
||||
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
## Purpose
|
||||
|
||||
Определяет независимое от открытых экранов часовое обновление личных инвестиционных данных и безопасно наблюдаемый результат последней попытки.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Часовой обход личных инвестиционных данных
|
||||
Система SHALL начинать первый часовой обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки этого подключения. После простоя система MUST выполнить не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока. Перекрывающиеся обходы MUST NOT выполняться: срок, наступивший во время выполняющегося обхода, MUST быть пропущен и MUST NOT ставиться в очередь; следующий обход становится допустимым только в следующий 60-минутный срок, отсчитанный от начала выполнявшейся попытки. Обход MUST обновлять доступные счета, Portfolio каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего Portfolio. Он MUST NOT повторно загружать полные прошлые интервалы операций, завершённые свечи или полную рыночную историю и MUST NOT зависеть от открытия frontend-экрана.
|
||||
|
||||
#### Scenario: Первое подключение
|
||||
- **WHEN** User успешно подключает T-Bank между часовыми сроками
|
||||
- **THEN** первый обход начинается без ожидания следующего календарного часа
|
||||
|
||||
#### Scenario: Возобновление после простоя
|
||||
- **WHEN** backend запускается после пропуска нескольких часовых сроков
|
||||
- **THEN** система выполняет один актуальный обход без синтетических прошлых обновлений
|
||||
|
||||
#### Scenario: Перекрытый срок
|
||||
- **WHEN** предыдущий обход всё ещё выполняется в момент очередного срока
|
||||
- **THEN** система не начинает второй обход и не ставит пропущенный обход в очередь
|
||||
|
||||
#### Scenario: Длительный обход пропускает один срок
|
||||
- **WHEN** обход начался в 10:00, всё ещё выполняется в 11:00 и завершился до 12:00
|
||||
- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00
|
||||
|
||||
### Requirement: Последний результат обхода доступен только для наблюдения
|
||||
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
|
||||
|
||||
#### Scenario: Частичный успех
|
||||
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
|
||||
- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт
|
||||
|
||||
#### Scenario: Наблюдение не запускает обход
|
||||
- **WHEN** User запрашивает результат последнего обхода
|
||||
- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Старая граница подключения
|
||||
- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода
|
||||
- **THEN** система не раскрывает результат прежней границы подключения
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
## Purpose
|
||||
|
||||
Определяет online-only получение справочных сведений и результатов поиска инструментов отдельно от сохранённых личных финансовых данных.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Справочные сведения и поиск инструментов получают онлайн
|
||||
Система SHALL получать подробные справочные сведения об Instrument и результаты поиска онлайн при их запросе. Такие сведения MUST NOT становиться самостоятельной долговременной копией справочника. Недоступность либо отсутствие онлайн-сведений MUST NOT задерживать сохранённый финансовый ответ, изменять его свежесть или подтверждать отсутствие Instrument.
|
||||
|
||||
#### Scenario: Медленный online lookup
|
||||
- **WHEN** подробные сведения об Instrument ещё не получены онлайн при чтении сохранённого Portfolio
|
||||
- **THEN** система возвращает доступный сохранённый Portfolio без ожидания lookup
|
||||
|
||||
#### Scenario: Online lookup не найден
|
||||
- **WHEN** online lookup не возвращает Instrument
|
||||
- **THEN** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
## Purpose
|
||||
|
||||
Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Полные оценки Portfolio сохраняются как история
|
||||
Система SHALL сохранять для каждого доступного Brokerage Account точку оценки по каждой исходной валюте только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение без валютной конвертации и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки.
|
||||
|
||||
#### Scenario: Полная мультивалютная оценка
|
||||
- **WHEN** успешный полный Portfolio содержит вычислимые итоги RUB и USD
|
||||
- **THEN** система сохраняет две отдельные точки с исходными значениями и валютами
|
||||
|
||||
#### Scenario: Неполная оценка
|
||||
- **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость
|
||||
- **THEN** система сохраняет новый Portfolio без точки истории оценки
|
||||
|
||||
### Requirement: История оценки скрыта вне подтверждённого доступа
|
||||
Система MUST сохранять историю оценки при замене token или удалении подключения, но MUST NOT раскрывать её без текущего подтверждённого доступа User к тому же Brokerage Account. Успешное новое подключение, подтверждающее доступ к счёту, MUST снова делать его сохранённую историю доступной для будущих поддерживаемых представлений. Система MUST NOT предоставлять отдельное публичное чтение точек истории этим требованием.
|
||||
|
||||
#### Scenario: Удалённое подключение
|
||||
- **WHEN** User удаляет T-Bank connection
|
||||
- **THEN** сохранённые точки оценки этого User не доступны для чтения
|
||||
|
||||
#### Scenario: Повторно подтверждённый счёт
|
||||
- **WHEN** новое успешное подключение подтверждает доступ User к тому же Brokerage Account
|
||||
- **THEN** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
## 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 }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio.
|
||||
|
||||
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection.
|
||||
|
||||
#### Scenario: Получение текущего Portfolio
|
||||
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён
|
||||
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta`
|
||||
|
||||
#### Scenario: Недоступный Brokerage Account
|
||||
- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection
|
||||
- **THEN** система отвечает `404` без Portfolio Snapshot
|
||||
|
||||
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||
Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST возвращать `{ data, meta }`, где `data` является сохранённым Snapshot, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot. Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника.
|
||||
|
||||
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента.
|
||||
|
||||
#### Scenario: Свежий Snapshot
|
||||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
|
||||
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Медленный lookup инструмента
|
||||
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
|
||||
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity
|
||||
|
||||
#### Scenario: Fallback после временной upstream-ошибки
|
||||
- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
|
||||
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
|
||||
|
||||
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при часовом обходе Portfolio
|
||||
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
|
||||
|
||||
#### Scenario: Неполная оценка при новом составе
|
||||
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
|
||||
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля
|
||||
|
||||
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||||
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд без automatic retry или refetch при возврате фокуса. Открытый экран MUST перечитывать сохранённый результат backend каждые 30 минут без запуска синхронизации T-Bank. Клиент MUST показывать freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность из `meta` текстом или доступно именованным элементом, а не только цветом. Клиент MUST NOT предоставлять действие ручной синхронизации Portfolio. При отсутствии ранее успешного ответа он MUST показывать структурный skeleton с доступным обозначением занятости без вымышленных сумм; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
|
||||
|
||||
Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями.
|
||||
|
||||
При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки.
|
||||
|
||||
#### Scenario: Показ устаревшего Portfolio
|
||||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||
- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации
|
||||
|
||||
#### Scenario: Первичная загрузка Portfolio
|
||||
- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа
|
||||
- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders
|
||||
|
||||
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
|
||||
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
|
||||
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки
|
||||
|
||||
#### Scenario: Недоступный счёт на клиенте
|
||||
- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404`
|
||||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
|
||||
|
||||
#### Scenario: Обновление после истечения срока свежести
|
||||
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание
|
||||
- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием
|
||||
|
||||
#### Scenario: Ошибка обновления после успешного ответа
|
||||
- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой
|
||||
- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения
|
||||
|
||||
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой
|
||||
- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению
|
||||
|
||||
#### Scenario: Повторное обновление успешно
|
||||
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
|
||||
- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale`
|
||||
|
||||
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
|
||||
- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Создание и замена подключения T-Bank
|
||||
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первый часовой обход без команды frontend. История стоимости прежних счетов MUST сохраняться, но MUST быть доступна только для счетов, доступ к которым подтверждён новым подключением.
|
||||
|
||||
#### Scenario: Успешное создание подключения без счетов
|
||||
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
|
||||
- **THEN** система сохраняет подключение, отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }` и не создаёт фиктивные счета
|
||||
|
||||
#### Scenario: Успешная замена подключения
|
||||
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
|
||||
- **THEN** система заменяет token, обновляет `connectedAt`, отвечает текущим статусом без token и начинает первый часовой обход нового подключения
|
||||
|
||||
#### Scenario: Невалидный token не заменяет существующее подключение
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
|
||||
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
|
||||
|
||||
#### Scenario: T-Bank временно недоступен при проверке token
|
||||
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
|
||||
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
|
||||
|
||||
### Requirement: Удаление подключения T-Bank
|
||||
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и сделать относящиеся к User текущие личные данные недоступными. История стоимости счёта MUST сохраняться, но MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
|
||||
|
||||
#### Scenario: Удаление существующего подключения
|
||||
- **WHEN** User удаляет существующее подключение
|
||||
- **THEN** система удаляет token, скрывает текущие личные данные и историю стоимости и отвечает `204`
|
||||
|
||||
#### Scenario: Повторное удаление подключения
|
||||
- **WHEN** User удаляет уже отсутствующее подключение
|
||||
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
## Purpose
|
||||
|
||||
Определяет единственного владельца обновления личных данных T-Bank и узкие
|
||||
исключения первой загрузки данных, которых ещё нет в локальном результате.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Часовой обход владеет обновлением личных данных T-Bank
|
||||
Система MUST обновлять личные инвестиционные данные T-Bank только часовым обходом для текущего подключения User. Обычные resource reads, действия повтора и открытие экранов MUST NOT запускать импорт, ручную личную синхронизацию или обращение к T-Bank. Единственными исключениями являются первая загрузка отсутствующего интервала Account Ledger и впервые открытого диапазона свечей: они MAY начать получение только этого отсутствующего интервала либо диапазона, когда для него нет подтверждённого локального результата. Эти исключения MUST NOT обновлять Accounts, Portfolio, иные интервалы операций, прогноз будущих событий или другой диапазон свечей. После подтверждения полноты запрошенного прошлого интервала или диапазона его повторное чтение MUST оставаться локальным.
|
||||
|
||||
#### Scenario: Обычное чтение не запускает импорт
|
||||
- **WHEN** User открывает Accounts, Portfolio, Account Ledger, будущие события либо повторяет неуспешное HTTP-чтение
|
||||
- **THEN** система читает локальный результат и не начинает личную синхронизацию T-Bank
|
||||
|
||||
#### Scenario: Первый отсутствующий интервал операций
|
||||
- **WHEN** User впервые открывает интервал Account Ledger без подтверждённого локального результата
|
||||
- **THEN** система MAY начинает получение только этого интервала и возвращает его локальное состояние неполноты до подтверждения результата
|
||||
|
||||
#### Scenario: Впервые открытый диапазон свечей
|
||||
- **WHEN** User впервые открывает диапазон свечей, для которого нет подтверждённого локального результата
|
||||
- **THEN** система MAY начинает получение только этого диапазона и не загружает другие диапазоны или полную историю свечей
|
||||
|
||||
### Requirement: Результат личного обновления остаётся наблюдаемым
|
||||
Каждый response resource read личных данных текущего подключения MUST возвращать `{ data, meta }`. `meta` MUST отдельно делать наблюдаемыми freshness и полноту именно `data`, момент последнего успешного полного результата resource, момент и состояние последней часовой попытки, затронувшей resource, и доступность resource. Состояние последней попытки MUST различать отсутствие попытки, выполнение, полный успех, частичный успех и неуспех. Доступность MUST различать доступный полный результат, первую загрузку без полного результата, временную недоступность без полного результата и недоступность из-за отсутствующего либо недействительного подключения. Неполный, продолжающийся или неуспешный результат MUST NOT обозначаться полным или свежим. Успешный пустой Accounts либо иной пустой полный результат MUST обозначаться доступным и полным, а не отсутствующим. При неуспешном обновлении последний подтверждённый полный результат и его время MUST оставаться доступными, когда отдельное требование capability не предписывает недоступность данных; его freshness MUST определяться исходным временем, а `meta` MUST отдельно обозначать неуспех последней попытки. Наблюдение этих состояний MUST NOT предоставлять способ начать синхронизацию.
|
||||
|
||||
#### Scenario: Ошибка после полного результата
|
||||
- **WHEN** очередное обновление набора личных данных завершается ошибкой после ранее подтверждённого полного результата
|
||||
- **THEN** система возвращает предыдущий полный `data`, его исходное время и freshness, а `meta` отдельно обозначает неуспех последней попытки
|
||||
|
||||
#### Scenario: Первая загрузка ещё не завершена
|
||||
- **WHEN** для набора личных данных ещё нет подтверждённого полного результата
|
||||
- **THEN** система возвращает `data: null` и в `meta` обозначает выполнение первой загрузки либо её неуспех без частичного результата, выданного за полный
|
||||
|
||||
#### Scenario: Успешный пустой список счетов
|
||||
- **WHEN** последний полный результат Accounts подтверждает отсутствие открытых поддерживаемых счетов
|
||||
- **THEN** система возвращает `data: []`, а `meta` обозначает доступный полный результат, его время и состояние последней часовой попытки
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# System Analysis
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
| Approved delta | System decision covered here |
|
||||
| --- | --- |
|
||||
| `hourly-investment-sync`: hourly walk and observable result | Per-connection durable hourly attempt, no catch-up or overlap, and `lastAttempt`/Accounts `sync` semantics. |
|
||||
| `tbank-data-sync`: one update owner and observable resource state | Local resource reads, two narrow first-load exceptions, and the target `{ data, meta }` contract. |
|
||||
| `brokerage-accounts` | Saved Accounts and per-account summaries, plus the connection-wide hourly result exposed as `sync`. |
|
||||
| `portfolio-view` and `portfolio-value-history` | Saved complete snapshots, stale fallback, connection-revision publication fence, and private retained value history. |
|
||||
| `brokerage-account-ledger` | First opening of an unconfirmed historical interval may load only that interval; complete history remains local. |
|
||||
| `brokerage-account-upcoming-events` | Saved forecast query/window semantics, complete-source publication, and independent failed-update status. |
|
||||
| `online-instrument-information` | Online reference lookup is separate from personal-resource reads and cannot delay or alter their status. |
|
||||
| `frontend-data-resilience` | A retry reissues only HTTP GET; frontend cache, server fallback, and hourly-attempt failure remain distinct. |
|
||||
| `tbank-connection` | PUT starts the new connection's first walk; replacement/deletion hides old personal data and old walk status. |
|
||||
|
||||
## Systems and Responsibilities
|
||||
|
||||
| System | Responsibility | Must not do |
|
||||
| --- | --- | --- |
|
||||
| Frontend | Read resources, render `data` and all observable `meta` fields, retain its own successful screen cache, and retry only the GET. | Start personal sync, infer freshness from a client error, or expose a manual personal-sync action. |
|
||||
| Backend resource API | Authenticate, enforce the current connection boundary, read local persisted resources, and return the target envelope. | Wait for or call T-Bank while serving ordinary resource GETs. |
|
||||
| Durable hourly scheduler | Reserve at most one current walk per connection; refresh membership, each available Portfolio, mutable operations tail, then events. | Replay missed terms, queue an overlapped term, or refresh complete historical ledger intervals/candle ranges. |
|
||||
| Local store | Keep last confirmed complete data, coverage, resource timestamps, hourly attempt/result, and connection revision. | Publish data or status from a superseded connection revision. |
|
||||
| T-Bank and public reference sources | Supply upstream data to connection PUT validation, the hourly walk, and explicitly online reference lookups. | Become a synchronous dependency of a normal personal resource GET. |
|
||||
|
||||
## Interaction Flows
|
||||
|
||||
`sequences/read-only-resource-get.mmd` defines the normal read and retry boundary. A `200` read uses only local state; a boundary error may occur before any data lookup.
|
||||
|
||||
`sequences/hourly-investment-walk.mmd` defines the sole normal personal-data updater. A successful connection starts its first attempt immediately. Thereafter a next attempt is eligible no earlier than 60 minutes after the preceding attempt started. Startup performs at most one current attempt, not a replay. If a term arrives while an attempt is running, it is dropped rather than queued; the next eligible term is measured from the running attempt's start.
|
||||
|
||||
The walk updates current membership, every available account's Portfolio, each account's mutable operations tail, and forecasts/events for instruments in the current Portfolio. Each successful section persists independently. A failed section preserves its earlier complete result and its original success time. Complete past operation intervals, completed candles, and full market history are not refreshed by the walk.
|
||||
|
||||
### First-load exceptions
|
||||
|
||||
1. Opening an unconfirmed historical Account Ledger interval may start retrieval of only that missing interval and immediately returns local rows plus incomplete coverage/status. It does not refresh membership, Accounts, Portfolio, events, another operations interval, or candles. A confirmed complete interval is never rechecked due to a GET or the hourly walk.
|
||||
2. Opening a candle range with no confirmed local result may start retrieval of only that range. It does not retrieve other ranges or all history; a confirmed range is subsequently local.
|
||||
|
||||
These are acquisition exceptions, not variants of manual sync. HTTP retry of either resource rereads its local state; it does not broaden or restart work.
|
||||
|
||||
## Contracts
|
||||
|
||||
`contracts/investment-resource-envelope.schema.json` is the minimal target cross-system artifact. It describes the changed resource envelope rather than duplicating backend `openapi.json`.
|
||||
|
||||
All affected personal resource successes use `{ data, meta }`: Accounts; account and `all` Portfolio; account and `all` operations; account and `all` events. `data` is the last confirmed complete result, except that ledger first-load may return locally held rows with `meta.completeness: "incomplete"`. Where no complete result exists, `data` is `null`; a partial result must never be represented as complete.
|
||||
|
||||
`meta` has five independent meanings:
|
||||
|
||||
| Field | Target meaning |
|
||||
| --- | --- |
|
||||
| `freshness` | `fresh`/`stale` applies to the timestamp of the returned confirmed result; `unknown` applies when none exists. A failed or running attempt never makes data fresh. |
|
||||
| `completeness` | `complete` only after the resource's required source result is confirmed. `incomplete` describes an in-progress/failed first-load or ledger coverage gap. |
|
||||
| `lastSuccessfulAt` | Timestamp of the returned complete result, or `null` when none exists. It is not replaced by a later failed attempt. |
|
||||
| `lastAttempt` | The resource-relevant hourly result: `none`, `running`, `succeeded`, `partial`, or `failed`, with nullable start/end timestamps and only safe failed section/account/error-code entries. `partial` means at least one section succeeded and at least one failed. |
|
||||
| `availability` | `available` has a complete result, including `[]`; `initial-loading` and `temporarily-unavailable` have no complete result; `connection-unavailable` means the current connection cannot expose the resource. |
|
||||
|
||||
Accounts also returns top-level `sync`: `null` if the current connection has no hourly attempt, otherwise the complete connection-wide last-walk status. Its `problems` list is the safe, scoped failure list for `membership`, `portfolio`, `operations`, and `events`; an account ID is present only for account-scoped sections. `sync` is observation only and is not a command or progress API.
|
||||
|
||||
Events preserves the existing account and `all` scope. `from` and `to` are optional strict `YYYY-MM-DD` UTC dates with inclusive bounds; omitted `from` is the current UTC date and omitted `to` is `from + 30` calendar days. `from > to` returns `400 INVALID_EVENTS_PERIOD`, does not read a substitute range, and starts no source work. A saved forecast retains `calculatedAt` and its original inclusive calculation window. The response filters events to the intersection of that original window and the requested window. Empty intersection and no matching events are successful empty `data` with the same forecast metadata. `all` is complete only if every included account has its required complete forecast sources; it cannot hide a missing account contribution. Events' `meta.lastAttempt` reports the resource-relevant hourly events result independently of the saved forecast, so a failed update can coexist with an available forecast.
|
||||
|
||||
## Data and State Semantics
|
||||
|
||||
An empty confirmed Accounts list, empty Portfolio, empty event intersection, and empty event list are all available complete results, never missing data. A stale complete result remains available indefinitely until a connection boundary makes it unavailable. `Portfolio.isStale` retains its snapshot-specific meaning and is not a substitute for `meta.lastAttempt` or a frontend request error.
|
||||
|
||||
Forecast publication requires complete broker and public-source results for the Portfolio Snapshot used to calculate it. It stores the Portfolio quantity basis, `calculatedAt`, and source window together. A failed refresh cannot publish a partial forecast or relabel an older forecast as calculated from a newer Portfolio.
|
||||
|
||||
Successful complete Portfolio evaluations may append one exact, per-currency value-history point at the actual successful Portfolio time. Missing valuation must not be replaced with zero and cannot create a point. Retry/publication of one attempt is idempotent. History survives connection replacement/deletion but is readable only after the current connection confirms access to the same account; no public history-read endpoint is introduced.
|
||||
|
||||
The connection revision is the publication fence. PUT validates the candidate token, atomically establishes a new revision and membership boundary, then triggers its first walk. DELETE is idempotent, prevents further token use, and hides current personal data. Old attempts cannot publish into a replacement connection, and neither old `sync` nor retained value history is disclosed across an unconfirmed boundary.
|
||||
|
||||
## Error and Recovery Semantics
|
||||
|
||||
Authentication errors remain HTTP boundary errors. No connection is `409 TBANK_NOT_CONNECTED`. A token known invalid/unauthorized from an hourly attempt is `400 INVALID_TBANK_TOKEN`; it retains the connection record but blocks personal-data disclosure and must not be masked by fallback data. A requested account outside current confirmed membership is `404`; unknown membership does not authorize an arbitrary account ID. Connection PUT validation failures remain `400 INVALID_TBANK_TOKEN` or `503 TBANK_UNAVAILABLE` and leave the prior connection unchanged.
|
||||
|
||||
Temporary upstream failure during an hourly attempt is represented in `meta.lastAttempt` and, on Accounts, `sync`; it is not a synchronous GET failure. With a previous complete result, GET returns `200` and the preserved data. Without one, GET returns `{ data: null, meta }` with `initial-loading` or `temporarily-unavailable`. `503 TBANK_UNAVAILABLE` remains reserved for an HTTP request that itself cannot be served, not a completed background failure.
|
||||
|
||||
Frontend recovery is limited to rereading the resource. It retains its Client Cached Snapshot during a background HTTP failure, while separately showing server stale/fallback and the failed hourly attempt where supplied. Replacing/deleting a connection invalidates personal client queries so a late response cannot revive hidden data.
|
||||
|
||||
## Observability Semantics
|
||||
|
||||
The durable hourly attempt is the observable unit: start time, terminal state, finish time, and safe failed section/account/error-code list. Absence of an attempt is `none` in resource `lastAttempt` and `null` in Accounts `sync`; it is not success. A running attempt has `finishedAt: null`. The status is scoped to the current connection revision and contains no token, upstream payload, cursor, or internal revision.
|
||||
|
||||
Resource `lastSuccessfulAt`, freshness, completeness, and availability describe returned `data`; the hourly result describes the attempt. This separation permits a truthful response after partial/failed refresh. Frontend status text or accessible names must expose all five resource meanings and must not rely on color alone.
|
||||
|
||||
## Security and NFR at System Boundaries
|
||||
|
||||
All personal reads require the authenticated User and current-connection access check. T-Bank tokens remain server-only, encrypted, and absent from response metadata, OpenAPI examples, logs, and status failures. Online instrument reference failure cannot prove an instrument absent or alter personal snapshot freshness.
|
||||
|
||||
The scheduler is a single durable reservation owner for the deployed single backend process. The target explicitly does not add multi-process scheduling. Local read latency is isolated from upstream latency because ordinary GETs do not invoke upstream personal data calls. The existing upstream limiter remains the boundary for PUT validation and hourly work.
|
||||
|
||||
## Compatibility / Migration
|
||||
|
||||
The current backend-generated `openapi.json` and frontend snapshot are identical and already expose resource envelopes, Accounts top-level `sync`, and events `sync=observe`; they are evidence of AS-IS, not the target authority. Current generated `InvestmentResourceMetaDto` uses `availability: missing|partial|complete` plus per-work `sync: idle|running|failed`, while `HourlySyncStatusDto` adds `cancelled`; these do not fully express the approved target's first-load availability, `none|succeeded|partial` attempt distinction, and resource-scoped last attempt. Current `sync=observe` and `/api/v1/tbank-sync/{id}` preserve legacy screen-work observation and must not be used to reintroduce request-started personal synchronization.
|
||||
|
||||
Implementation must evolve backend Swagger DTOs/controllers and regenerate backend OpenAPI. The frontend must receive that published snapshot and regenerate its local transport types. The HTTP route identities and established Problem Details codes remain compatible unless the generated target contract explicitly changes them; no copy of the full OpenAPI belongs in this change. The target schema is intentionally additive/semantic and must be reconciled with generated field names before release.
|
||||
|
||||
## Durable Documentation Candidates
|
||||
|
||||
- `docs/adr/019-hourly-reads-and-online-instrument-reference.md`: clarify that hourly status is connection-scoped and that only the two first-load exceptions may acquire personal/market data.
|
||||
- `docs/architecture/cross-cutting/data-freshness.md`: replace legacy request-driven freshness table and `sync=observe` language with target resource/attempt separation and `data: null` first-load states.
|
||||
- `docs/domains/portfolio/flows/portfolio-loading.md`: replace screen-triggered refresh/status flow with local GET and hourly-walk flow.
|
||||
- `docs/domains/income/upcoming-events.md`: describe saved forecast source-window intersection, `calculatedAt`, and independent failed hourly update state.
|
||||
- `docs/adr/017-local-tbank-reads-and-background-sync.md`: mark its request-driven/manual-refresh decisions superseded by ADR 019 rather than leaving competing guidance.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None. The approved Proposal and Delta Specs determine the target semantics; implementation will reconcile field naming and route annotations through backend-generated OpenAPI without treating current generated artifacts as a second authority.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "reconcile-hourly-investment-sync/investment-resource-envelope.schema.json",
|
||||
"title": "Target investment resource envelope",
|
||||
"description": "Target cross-system semantics only; the backend-generated OpenAPI remains the executable HTTP contract.",
|
||||
"type": "object",
|
||||
"required": ["data", "meta"],
|
||||
"properties": {
|
||||
"data": {
|
||||
"description": "The last confirmed complete result, an allowed incomplete ledger first-load result, or null when no complete result is available."
|
||||
},
|
||||
"meta": { "$ref": "#/$defs/resourceMeta" },
|
||||
"sync": { "$ref": "#/$defs/hourlyWalk" }
|
||||
},
|
||||
"$defs": {
|
||||
"resourceMeta": {
|
||||
"type": "object",
|
||||
"required": ["freshness", "completeness", "lastSuccessfulAt", "lastAttempt", "availability"],
|
||||
"properties": {
|
||||
"freshness": { "enum": ["fresh", "stale", "unknown"] },
|
||||
"completeness": { "enum": ["complete", "incomplete"] },
|
||||
"lastSuccessfulAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"lastAttempt": { "$ref": "#/$defs/attempt" },
|
||||
"availability": { "enum": ["available", "initial-loading", "temporarily-unavailable", "connection-unavailable"] }
|
||||
}
|
||||
},
|
||||
"attempt": {
|
||||
"type": "object",
|
||||
"required": ["state", "startedAt", "finishedAt", "problems"],
|
||||
"properties": {
|
||||
"state": { "enum": ["none", "running", "succeeded", "partial", "failed"] },
|
||||
"startedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"finishedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"problems": { "type": "array", "items": { "$ref": "#/$defs/problemSection" } }
|
||||
}
|
||||
},
|
||||
"hourlyWalk": {
|
||||
"description": "Present only on Accounts; absent from other resource envelopes.",
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{ "$ref": "#/$defs/attempt" }
|
||||
]
|
||||
},
|
||||
"problemSection": {
|
||||
"type": "object",
|
||||
"required": ["section", "accountId", "errorCode"],
|
||||
"properties": {
|
||||
"section": { "enum": ["membership", "portfolio", "operations", "events"] },
|
||||
"accountId": { "type": ["string", "null"] },
|
||||
"errorCode": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
sequenceDiagram
|
||||
participant Scheduler
|
||||
participant DB as Durable reservation/status
|
||||
participant TB as T-Bank and public sources
|
||||
participant Store as Local resource store
|
||||
participant API as Resource GET
|
||||
|
||||
Scheduler->>DB: Reserve current connection walk
|
||||
alt Due and no walk running
|
||||
DB-->>Scheduler: Attempt with connection revision
|
||||
Scheduler->>TB: Refresh membership, portfolios, mutable operations, events
|
||||
Scheduler->>Store: Publish each complete successful result for the same revision
|
||||
Scheduler->>DB: Settle succeeded, partial, or failed with safe failed sections
|
||||
else Not due or an attempt is running
|
||||
DB-->>Scheduler: No work
|
||||
end
|
||||
API->>Store: Read persisted result and attempt status
|
||||
Store-->>API: Last confirmed data and metadata
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant UI as Frontend
|
||||
participant API as Backend resource GET
|
||||
participant DB as Local store
|
||||
participant TB as T-Bank
|
||||
|
||||
User->>UI: Open screen or retry HTTP read
|
||||
UI->>API: GET resource
|
||||
API->>DB: Authorize current connection and read saved result/status
|
||||
DB-->>API: data, resource state, last hourly attempt
|
||||
API-->>UI: 200 { data, meta } or boundary Problem Details
|
||||
Note over API,TB: No T-Bank call and no personal sync work from GET or retry
|
||||
@@ -0,0 +1,67 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Backend runtime and published contract
|
||||
|
||||
- [ ] 1.1 [backend] Привести durable hourly walk, connection-revision fence и локальные resource readers к утверждённой модели: только hourly owner для обычного обновления, узкие first-load исключения, сохранение complete fallback и изолированное наблюдение resource/connection attempt.
|
||||
- **Covers:** `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `portfolio-value-history` — `Полные оценки Portfolio сохраняются как история`; `portfolio-value-history` — `История оценки скрыта вне подтверждённого доступа`; `online-instrument-information` — `Справочные сведения и поиск инструментов получают онлайн`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank`
|
||||
- **Source:** System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics, Security and NFR at System Boundaries; Design §§ Backend: retain storage owners and publication fences, Data / Storage Changes, Developer Verification Strategy; QA Analysis §§ Coverage Matrix, Cross-system Verification.
|
||||
- **Verification:** Run the backend's relevant runtime, SQLite/migration, controller, and E2E tests proving local ordinary GET/retry has no personal upstream call; hourly cadence and dropped overlaps; narrow first-load acquisition; persisted fallback/partial results; connection replacement/deletion fences; events windows; and no token or internal details in observable status.
|
||||
- **Depends on:** none
|
||||
|
||||
- [ ] 1.2 [backend] Replace the shared investment resource DTO/mapper and endpoint responses with the target `{ data, meta }` projection, Accounts-only connection `sync`, then regenerate the backend OpenAPI from Swagger annotations.
|
||||
- **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Forecasts preserve event meaning and uncertainty`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
|
||||
- **Source:** System Analysis §§ Contracts, Compatibility / Migration; Design §§ Backend: one resource-meta projection and one connection-walk projection, Contract: Swagger is produced at the DTO boundary, Contract Implementation; QA Analysis §§ Cross-system Verification, Regression Impact.
|
||||
- **Verification:** Regenerate backend `openapi.json` and run the backend's OpenAPI contract/type/lint tests plus runtime response tests. Confirm semantic compatibility with `system-analysis/contracts/investment-resource-envelope.schema.json`: nullable `data`; all five required `meta` meanings; `none|running|succeeded|partial|failed`; nullable attempt fields; Accounts-only nullable top-level `sync`; established routes and Problem Details; and `sync=observe` never starting source work.
|
||||
- **Depends on:** 1.1
|
||||
|
||||
## 2. Frontend generated consumer and states
|
||||
|
||||
- [ ] 2.1 [frontend] Receive the published backend OpenAPI snapshot and regenerate the local transport types; project its target resource envelope directly through the existing resource-query seam without handwritten parallel types or compatibility normalization.
|
||||
- **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`
|
||||
- **Source:** System Analysis § Compatibility / Migration; Design §§ Contract: Swagger is produced at the DTO boundary, Frontend: one query cache and metadata-aware screen state; QA Analysis §§ Cross-system Verification, Regression Impact.
|
||||
- **Verification:** Run `openapi:check` and the frontend typecheck; verify the committed generated snapshot is the backend-published artifact and generated types expose nullable `data`, required target metadata, and Accounts-only `sync`.
|
||||
- **Depends on:** 1.2
|
||||
|
||||
- [ ] 2.2 [frontend] Update Accounts, Portfolio, operations, and events query owners, page VMs, and views to render the five metadata meanings and separate server attempt, server stale, and client HTTP-read states; retain successful snapshots, keep retry/reread HTTP-only, and preserve access invalidation.
|
||||
- **Covers:** `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`
|
||||
- **Source:** System Analysis §§ Error and Recovery Semantics, Observability Semantics; Design §§ Frontend: one query cache and metadata-aware screen state, UI/UX Package, Developer Verification Strategy; QA Analysis §§ Coverage Matrix: Frontend recovery and accessibility, Manual / Exploratory Scope.
|
||||
- **Verification:** Run frontend unit/component tests and configured UI checks covering no automatic retry, retry as GET only, 30-minute reread, cache retention after background error, connection invalidation, no-data/empty/loaded states, distinct stale/attempt/transport warnings, keyboard-accessible status text, and narrow viewport wrapping.
|
||||
- **Depends on:** 2.1
|
||||
|
||||
## 3. Durable AS-IS documentation
|
||||
|
||||
- [ ] 3.1 [docs] Update `architecture/cross-cutting/data-freshness.md` with the approved hourly-owner, local-read, first-load-exception, and independent resource/attempt/client-state model.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `architecture/cross-cutting/data-freshness.md`.
|
||||
- **Verification:** Compare the page against delivered runtime responses and UI: ordinary GET/retry has no personal upstream call; target generated-contract meanings, stale fallback, no-result, client read failure, and hourly failure remain distinct.
|
||||
- **Depends on:** 1.2, 2.2
|
||||
|
||||
- [ ] 3.2 [docs] Update `domains/portfolio/flows/portfolio-loading.md` with read-only saved snapshot loading, 30-minute local rereads, fallback semantics, and connection/revision disclosure fences.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `domains/portfolio/flows/portfolio-loading.md`.
|
||||
- **Verification:** Compare the page with the delivered Portfolio route and screen: GET/retry does not call T-Bank or expose manual sync; no-data, empty, stale, failed hourly attempt, and failed HTTP reread remain distinct.
|
||||
- **Depends on:** 1.2, 2.2
|
||||
|
||||
- [ ] 3.3 [docs] Update `domains/income/upcoming-events.md` with saved forecast window intersection, `calculatedAt`, independent failed-update state, and empty/no-result semantics.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `domains/income/upcoming-events.md`.
|
||||
- **Verification:** Compare the page with delivered account and `all` events responses/screens: invalid periods return `INVALID_EVENTS_PERIOD` without source work, window intersection preserves forecast metadata, and empty intersection differs from failed-refresh fallback.
|
||||
- **Depends on:** 1.2, 2.2
|
||||
|
||||
- [ ] 3.4 [docs] Update the investment section of `architecture/http-api.md` to identify backend-generated OpenAPI as the executable `{ data, meta }` contract and remove the request-started screen-sync model from AS-IS guidance.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `architecture/http-api.md`.
|
||||
- **Verification:** Compare prose and examples with the backend-generated OpenAPI and runtime responses: target nullability/enums and required metadata match, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work.
|
||||
- **Depends on:** 1.2
|
||||
|
||||
- [ ] 3.5 [docs] Update `adr/017-local-tbank-reads-and-background-sync.md` to preserve historical context while explicitly superseding its request-driven/manual-refresh decisions through ADR 019.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `adr/017-local-tbank-reads-and-background-sync.md`.
|
||||
- **Verification:** Confirm ADR 017 explicitly identifies superseded decisions, links to ADR 019 and current freshness documentation, and no longer presents manual/request-driven sync as current architecture.
|
||||
- **Depends on:** 3.1, 3.6
|
||||
|
||||
- [ ] 3.6 [docs] Update `adr/019-hourly-reads-and-online-instrument-reference.md` with the current-connection-scoped hourly result, ordinary read-only GET/retry boundary, two narrow acquisition exceptions, and observation-not-command rule.
|
||||
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
|
||||
- **Source:** Docs Impact § Documentation Changes: `adr/019-hourly-reads-and-online-instrument-reference.md`.
|
||||
- **Verification:** Confirm ADR 019 describes only the delivered hourly model and bounded exceptions, agrees with the updated freshness page, and does not introduce another HTTP contract source.
|
||||
- **Depends on:** 1.2, 2.2
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-10-07
|
||||
@@ -0,0 +1,104 @@
|
||||
## Context
|
||||
|
||||
`useScreenSync` уже является общим module для экранной синхронизации. Его
|
||||
interface принимает scope и набор resource queries; реализация опрашивает
|
||||
статусы выполняющейся работы и вызывает `observe()` для ресурсов того же
|
||||
экрана. Один интервал в 1 секунду управляет обоими видами запросов, поэтому
|
||||
страницы с несколькими ресурсами умножают фоновый трафик.
|
||||
|
||||
Нужна меньшая частота обновления при сохранении текущей cache identity,
|
||||
автоматического восстановления, защиты доступа и итогового обновления сразу
|
||||
после завершения работы.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Сократить периодический HTTP-трафик выполняющейся экранной синхронизации
|
||||
примерно в пять раз.
|
||||
- Сохранить небольшую и прежнюю interface `useScreenSync` для всех page VMs.
|
||||
- Сразу обновлять ресурсы после terminal state синхронизации.
|
||||
- Проверять поведение на HTTP seam, а не детали реализации TanStack Query.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Изменять backend status contract, resource endpoints или модель синхронизации.
|
||||
- Вводить отдельный polling module, второй cache или push-канал.
|
||||
- Менять пользовательское представление загрузки и ошибок.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Preserve one screen-sync module and lengthen its interval
|
||||
|
||||
`useScreenSync` остаётся единственным module, который владеет связью между
|
||||
статусом экранной синхронизации и observe reads. Его implementation заменяет
|
||||
интервал status polling с одной секунды на пять секунд. Поскольку effect уже
|
||||
наблюдает ресурсы после нового status response, это одним изменением
|
||||
ограничивает оба потока запросов без расширения interface для callers.
|
||||
|
||||
Alternative: добавить второй timer только для observe reads при status polling
|
||||
раз в секунду. Отклонено: status traffic останется частым, а два независимых
|
||||
ритма увеличат implementation и риск дублирующих наблюдений без требуемой
|
||||
пользы.
|
||||
|
||||
Alternative: читать ресурсы только после terminal state. Отклонено: это
|
||||
устранит промежуточную публикацию уже готовых данных и изменит существующее
|
||||
поведение сильнее, чем требуется.
|
||||
|
||||
### Keep completion observation driven by the terminal status response
|
||||
|
||||
Текущий effect уже distinguishes running и completed status: для terminal state
|
||||
он observes every resource, включая тот, который был fetching. Этот путь
|
||||
сохраняется. Terminal response не ждёт отдельного timer, поэтому итоговый
|
||||
результат появляется в обработке того же status response; максимальная
|
||||
задержка возникает только до следующей пятиисекундной проверки.
|
||||
|
||||
### Test the public HTTP outcome at the screen-sync seam
|
||||
|
||||
Colocated tests `screen-sync` будут использовать MSW и fake timers, чтобы
|
||||
проверить отсутствие повторного status/read traffic до пяти секунд, следующий
|
||||
цикл после интервала и одно итоговое observe read при terminal status. Existing
|
||||
tests продолжат доказывать, что уход с экрана останавливает polling и что
|
||||
observe reads не создают новую импортирующую работу.
|
||||
|
||||
## UI/UX Applicability and Package
|
||||
|
||||
**Applicability and scope:** N/A. Изменяется только частота невидимых фоновых
|
||||
запросов; экраны, состояния, interactions и responsive behavior не меняются.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Status | Architectural outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| frontend | affected | `useScreenSync` реже опрашивает статус и тем самым реже наблюдает ресурсы, сохраняя немедленное обновление после terminal state. |
|
||||
| backend | N/A | Status и resource contracts не меняются. |
|
||||
| specs | affected | Delta фиксирует пятиисекундную верхнюю частоту и итоговое наблюдение после terminal state. |
|
||||
| docs | N/A | Пользовательские и эксплуатационные инструкции не меняются. |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Промежуточно готовые данные могут появиться до четырёх секунд позже, чем
|
||||
раньше; пользователь явно выбрал меньший трафик в обмен на эту задержку.
|
||||
- Terminal state обнаруживается следующей status-проверкой, поэтому его
|
||||
отображение также может ждать до пяти секунд после фактического завершения
|
||||
backend work. После обнаружения дополнительной задержки нет.
|
||||
- Смена только общей константы затрагивает все экраны-потребители; тесты
|
||||
module защищают единое ожидаемое поведение.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Изменить policy interval у существующего screen-sync module.
|
||||
2. Адаптировать и дополнить его seam tests для пятиисекундного ритма и
|
||||
terminal refresh.
|
||||
3. Выполнить локальные frontend quality gates перед поставкой.
|
||||
|
||||
### Rollback Contract
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `N/A` |
|
||||
| Reason | Изменение локальной frontend polling policy не меняет данные, публичный contract, security или миграции; rollback состоит в возврате прежнего значения интервала. |
|
||||
| Trigger | N/A |
|
||||
| Actions | N/A |
|
||||
| Data safety | N/A |
|
||||
| Verification | N/A |
|
||||
@@ -0,0 +1,50 @@
|
||||
## Why
|
||||
|
||||
Пока экран ожидает синхронизацию T-Bank, клиент опрашивает её состояние каждую
|
||||
секунду. Каждый новый ответ запускает наблюдение всех ресурсов открытого экрана.
|
||||
На экранах с несколькими финансовыми выборками это создаёт непропорционально
|
||||
много запросов при том, что пользователь готов увидеть промежуточный результат
|
||||
с небольшой задержкой.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Во время выполняющейся экранной синхронизации приложение реже проверяет её
|
||||
состояние и обновляет наблюдаемые финансовые ресурсы не чаще одного раза за
|
||||
пять секунд.
|
||||
- После terminal state синхронизации приложение сразу наблюдает ресурсы
|
||||
открытого экрана, чтобы отобразить итоговый результат без дополнительной
|
||||
задержки.
|
||||
- Существующие состояния ожидания, доступные частичные результаты,
|
||||
автоматическое восстановление чтения и прекращение работы после ухода с
|
||||
экрана сохраняются.
|
||||
|
||||
Ожидаемый результат: длительная синхронизация создаёт существенно меньше
|
||||
фоновых HTTP-запросов, а пользователь получает промежуточные результаты с
|
||||
задержкой не более пяти секунд и итоговые данные сразу после завершения.
|
||||
|
||||
Non-goals:
|
||||
|
||||
- Изменять запуск синхронизации, её scope, доступ к данным или финансовые
|
||||
расчёты.
|
||||
- Добавлять новые пользовательские элементы, ручное управление частотой или
|
||||
отдельный канал доставки изменений.
|
||||
- Гарантировать обновления финансовых данных в реальном времени.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
<!-- None. -->
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `specs/frontend-data-resilience/spec.md`: ограниченная частота наблюдения
|
||||
ресурсов во время выполняющейся синхронизации и немедленное наблюдение после
|
||||
terminal state.
|
||||
|
||||
## Impact
|
||||
|
||||
Изменение затрагивает финансовые экраны, использующие экранную синхронизацию:
|
||||
счета, детали счёта, операции, доходы, события и страницы инструментов. Оно
|
||||
сокращает фоновый HTTP-трафик при продолжающейся синхронизации, сохраняя
|
||||
автоматическое отображение готовых данных и существующие правила устойчивости.
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Временные сбои чтения восстанавливаются автоматически
|
||||
|
||||
Frontend SHALL автоматически повторять временно неуспешное чтение и
|
||||
восстанавливать получение необходимых текущему экрану данных без ручного
|
||||
обновления. Повторы одной попытки MUST иметь конечный предел и паузы,
|
||||
исключающие непрерывную череду запросов. При исчерпании предела доступные данные
|
||||
SHALL сохраняться, а при их отсутствии SHALL показываться локальная
|
||||
недоступность. Возвращение сети, возвращение пользователя к открытому экрану и
|
||||
повторное открытие экрана MUST возобновлять получение отсутствующих или
|
||||
требующих обновления данных без перезагрузки страницы. Свежий полный результат
|
||||
MUST NOT требовать обращения к источнику только из-за такого события.
|
||||
|
||||
Наблюдая выполняющуюся экранную синхронизацию, frontend MUST проверять её
|
||||
состояние и повторно читать связанные ресурсы не чаще одного раза за пять
|
||||
секунд. После получения terminal state этой синхронизации frontend MUST сразу
|
||||
наблюдать ресурсы текущего экрана, не ожидая следующего пятиисекундного
|
||||
интервала.
|
||||
|
||||
Восстановление MUST учитывать сбой чтения результата и сбой самого получения
|
||||
данных из источника, даже если чтение состояния завершилось успешно. Наблюдение
|
||||
уже выполняющейся работы MUST NOT запускать её повторно. Уход с экрана MUST
|
||||
прекращать новые попытки его восстановления; это не требует отменять уже
|
||||
начатую работу источника. Автоматическое восстановление MUST NOT повторять
|
||||
запросы, требующие исправления ввода, входа или восстановления доступа, и MUST
|
||||
NOT переносить старые параметры в новую пользовательскую выборку.
|
||||
|
||||
#### Scenario: Кратковременный сбой чтения
|
||||
- **WHEN** чтение текущей выборки временно не удалось, а следующая разрешённая
|
||||
автоматическая попытка успешна
|
||||
- **THEN** экран самостоятельно показывает результат без ручного действия и без
|
||||
сообщения о сбое или восстановлении
|
||||
|
||||
#### Scenario: Экранная синхронизация продолжается
|
||||
- **WHEN** экран наблюдает синхронизацию, которая остаётся в состоянии running
|
||||
после очередной проверки
|
||||
- **THEN** frontend выполняет следующую проверку состояния и наблюдение
|
||||
связанных ресурсов не ранее чем через пять секунд
|
||||
|
||||
#### Scenario: Экранная синхронизация завершилась
|
||||
- **WHEN** очередная проверка экранной синхронизации возвращает terminal state
|
||||
- **THEN** frontend сразу наблюдает связанные ресурсы текущего экрана, чтобы
|
||||
показать итоговый результат без ожидания следующего интервала
|
||||
|
||||
#### Scenario: Источник завершил работу ошибкой
|
||||
- **WHEN** чтение сохранённых данных успешно, но получение необходимых новых
|
||||
данных из источника временно не удалось
|
||||
- **THEN** приложение применяет ограниченное автоматическое восстановление к
|
||||
неуспешной работе, а не считает успешное чтение доказательством её успеха
|
||||
|
||||
#### Scenario: Повторы исчерпаны
|
||||
- **WHEN** временная недоступность сохраняется после конечного предела
|
||||
автоматических попыток
|
||||
- **THEN** приложение прекращает серию повторов, сохраняет доступный результат
|
||||
либо локальную недоступность без бесконечной загрузки и ожидает следующего
|
||||
события восстановления
|
||||
|
||||
#### Scenario: Сеть или пользователь вернулись
|
||||
- **WHEN** после временного сбоя восстановилась сеть либо пользователь вернулся
|
||||
к экрану с отсутствующим или требующим обновления результатом
|
||||
- **THEN** получение данных возобновляется автоматически для текущей выборки и
|
||||
при успехе заменяет состояние недоступности содержимым
|
||||
|
||||
#### Scenario: Несколько событий восстановления совпали
|
||||
- **WHEN** сеть восстановилась одновременно с возвращением пользователя на экран
|
||||
- **THEN** приложение не запускает дублирующую работу получения одного набора
|
||||
данных
|
||||
|
||||
#### Scenario: Работа источника ещё выполняется
|
||||
- **WHEN** приложение восстанавливает чтение состояния уже выполняющейся работы
|
||||
- **THEN** оно продолжает получение её результата без повторного запуска той же
|
||||
работы
|
||||
|
||||
#### Scenario: Пользователь покинул экран
|
||||
- **WHEN** пользователь ушёл со страницы с ожидающим восстановлением
|
||||
- **THEN** новые попытки для покинутого экрана прекращаются, а уже начатая работа
|
||||
не изменяет содержимое другой выборки
|
||||
|
||||
#### Scenario: Ошибка требует действия пользователя
|
||||
- **WHEN** подтверждены недействительное подключение, отсутствие доступа или
|
||||
некорректный ввод
|
||||
- **THEN** автоматическая серия повторов прекращается и появляется
|
||||
соответствующее действие или локальная ошибка без технической кнопки повторной
|
||||
загрузки
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. Screen synchronization policy
|
||||
|
||||
- [x] 1.1 [frontend] Change the screen-sync status polling policy from one
|
||||
second to five seconds while keeping the existing `useScreenSync` interface
|
||||
and terminal-state resource observation.
|
||||
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`
|
||||
- **Verification:** Run the colocated screen-sync test file and confirm its
|
||||
MSW assertions show no subsequent status or observe request before five
|
||||
seconds, with resources observed on a terminal status response.
|
||||
|
||||
- [x] 1.2 [frontend] Update colocated screen-sync tests with fake timers to
|
||||
cover the five-second running cadence, immediate terminal observation, and
|
||||
existing stop-on-unmount behavior.
|
||||
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`
|
||||
- **Verification:** `nvm use --silent && npm test -- screen-sync.test.tsx`
|
||||
|
||||
## 2. Frontend delivery
|
||||
|
||||
- [x] 2.1 [frontend] Run the frontend's canonical verification after the
|
||||
polling-policy and regression-test changes.
|
||||
- **Covers:** `N/A` — This task verifies the affected frontend delivery; it
|
||||
introduces no separate observable behavior beyond the requirement covered
|
||||
by tasks 1.1 and 1.2.
|
||||
- **Verification:** `nvm use --silent && npm run verify`
|
||||
|
||||
- [x] 2.2 [specs] Verify the completed OpenSpec change against the
|
||||
`wealth-ledger-specs` store before implementation review.
|
||||
- **Covers:** `N/A` — This task validates the planning artifacts and their
|
||||
traceability; it does not introduce runtime behavior.
|
||||
- **Verification:** `openspec validate --change reduce-screen-sync-polling --store wealth-ledger-specs`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd-v2
|
||||
created: 2026-10-12
|
||||
@@ -0,0 +1,220 @@
|
||||
# Engineering Design
|
||||
|
||||
## Context and Constraints
|
||||
|
||||
- Один backend-процесс; durable fence — reservation попытки в SQLite; таймер
|
||||
процесса только предлагает работу (существующий инвариант, комментарии в
|
||||
`tbank-hourly.scheduler.ts`).
|
||||
- Итог попытки пишется CAS-обновлением только из состояния `running`:
|
||||
устаревшая публикация после появления преемника уже отклоняется существующим
|
||||
fence (`settleHourly` where `state: 'running'`; `read()` при несовпадении
|
||||
`progress.attempts` помечает шаг `cancelled`).
|
||||
- Сегодня итог вычисляется по in-memory списку ошибок walk (пуст при
|
||||
исключении — ложный `succeeded`), ошибка `settleHourly` подавляется
|
||||
`.catch(() => undefined)`, `settledWorks` поллит без deadline, а id строк
|
||||
шагов не персистятся на hourly-попытке (`progress: { sections: [] }`).
|
||||
- Финализаторы enrichment/operations могут очистить память без durable finish —
|
||||
шаг остаётся `running`.
|
||||
- Топология не меняется: без lease и мультреплик; CAS-ограничения
|
||||
`attempt`/`revision`/`source` не ослабляются.
|
||||
|
||||
## Technical Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
|
||||
- Итог попытки выводится из durable-состояния шагов; ложный `succeeded`
|
||||
невозможен ни на одном error-пути.
|
||||
- Каждая попытка достигает терминального итога в ограниченное время
|
||||
(ограничение ожидания на каждый шаг).
|
||||
- Попытка с утраченным выполнением закрывается диагностируемо (`INTERRUPTED`)
|
||||
и не блокирует следующий обход дольше обычного часового срока.
|
||||
- Поздняя публикация шага текущей попытки сохраняется до фиксации итога;
|
||||
после появления преемника отклоняется существующим fence.
|
||||
|
||||
**Non-Goals**
|
||||
|
||||
- Lease, очередь, вторая реплика, параллельный scheduler.
|
||||
- Отмена уже начатых обращений к T-Bank.
|
||||
- Новый публичный статус попытки и изменения HTTP contract.
|
||||
- Graceful drain — только если тест контролируемой остановки докажет
|
||||
необходимость.
|
||||
|
||||
## Implementation Impact Map
|
||||
|
||||
### main
|
||||
|
||||
`N/A` — workspace/dev/release orchestration не меняется.
|
||||
|
||||
### frontend
|
||||
|
||||
`N/A` — публичный словарь состояний и форма `meta` не меняются.
|
||||
|
||||
### backend
|
||||
|
||||
`affected` — координатор `tbank-hourly.scheduler.ts` (walk/settle/
|
||||
settledWorks/launch), `tbank-sync.repository.ts` (`reserveHourly`/
|
||||
`settleHourly`), персист id шагов на попытке, durable finish в финализаторах
|
||||
`catalog-enrichment.service.ts` и `brokerage-operations-sync.service.ts`,
|
||||
изолированные SQLite-тесты reservation/coordinator.
|
||||
|
||||
### qa
|
||||
|
||||
`N/A` — QA repository не существует; системная верификация остаётся ручной по
|
||||
QA Analysis.
|
||||
|
||||
## Decisions
|
||||
|
||||
### backend
|
||||
|
||||
1. **Итог выводится из durable-состояния шагов (корневой фикс).**
|
||||
`settleHourly` в одной транзакции читает строки шагов попытки и вычисляет
|
||||
итог: `succeeded` — только когда каждый обязательный шаг успешен и закрытие
|
||||
инициировано нормальным завершением walk; `partial` — успешны некоторые;
|
||||
`failed` — ни один. Вызывающие больше не передают результат и список
|
||||
ошибок; walk передаёт максимум `cause: 'INTERRUPTED'` (закрытие до
|
||||
нормального завершения), который капит итог до `partial`. Исключение —
|
||||
обязательные секции, чей dispatch не создал durable-строку шага: walk
|
||||
передаёт их как дополнительные problem-записи (наблюдаемый факт
|
||||
диспатча, не результат), иначе попытка могла бы получить ложный успех.
|
||||
Альтернатива
|
||||
«прокидывать настоящий failures-map в catch» отклонена: не закрывает
|
||||
lost-settle и краш-пути — каждый error-путь требовал бы своего guard.
|
||||
Отдельный janitor-сервис отклонён: issue требует чинить существующих
|
||||
владельцев, не вводить параллельный scheduler.
|
||||
|
||||
2. **Id шагов персистятся на попытке в момент dispatch.** `progress`
|
||||
hourly-попытки переиспользует существующую форму `children: string[]` +
|
||||
`attempts: { [id]: attemptId }` (паттерн `ScreenProgress`/`progress()`).
|
||||
Walk записывает id каждой строки шага сразу после dispatch, до ожидания —
|
||||
иначе recovery после краша не знает множество шагов попытки, а шаг, упавший
|
||||
в dispatch, исчезает из множества.
|
||||
|
||||
3. **Ограниченное ожидание на каждый шаг.** `settledWorks` получает per-id
|
||||
deadline: константа `WORK_WAIT_BOUND_MS = 15 * 60 * 1000`; истёкший id
|
||||
возвращается как проблемный шаг с причиной `DEADLINE`. Тем же bound покрыты
|
||||
и ожидания walk до появления durable-строк шагов (review-замечание,
|
||||
follow-up WealthLedger/backend#78): `membership.request`/`member.done`,
|
||||
`access.local` и dispatch `portfolio`/`operations`/`events.refresh` гоняются
|
||||
с `WORK_WAIT_BOUND_MS` через race; истечение трактуется как проблемная
|
||||
секция (как failed dispatch), для `access.local` — как закрытие
|
||||
`INTERRUPTED`. Вызов не отменяется; поздняя durable-строка fenced по
|
||||
`attemptId`, как любая поздняя публикация. Управляемые часы в
|
||||
тестах — jest fake timers, инъекция clock в production-код не вводится.
|
||||
RPC не отменяется; поздний результат шага сохраняется его собственной
|
||||
строкой — учтён ли он в итоге, решает fence и момент фиксации.
|
||||
|
||||
4. **In-memory активные обходы + recovery на существующем tick.**
|
||||
`TbankHourlyScheduler` держит `active: Set<string>` (attemptId): добавление
|
||||
в начале `walk`, удаление в `finally`. `launch()` перед `reserveHourly`
|
||||
вызывает `recoverAbandoned(userId)`: если строка hourly `running`,
|
||||
`processRunId` свой и attemptId не в `active` — попытка потеряла settle и
|
||||
закрывается через `settleHourly` с `cause: 'INTERRUPTED'` (итог из детей,
|
||||
максимум `partial`). Это же закрывает застрявшие legacy-строки `running`.
|
||||
Новых таймеров и санитаров нет — восстановление едет на существующем
|
||||
минутном tick.
|
||||
|
||||
5. **Restart-ветка `reserveHourly` закрывает попытку итогом.** Существующая
|
||||
ветка «running + чужой `processRunId`» вместо голой записи
|
||||
`state: 'cancelled'` выполняет тот же derived settle с
|
||||
`cause: 'INTERRUPTED'` внутри транзакции reservation; далее прежний
|
||||
`due`-гейт: новый обход — не ранее следующего часового срока от начала
|
||||
попытки. `cancelled` для hourly-строк перестаёт писаться; существующие
|
||||
маппинги `cancelled` в `hourly()`/`hourlyAttempt()` остаются толерантным
|
||||
чтением и не удаляются.
|
||||
|
||||
6. **Причина прерывания — в существующем поле `errorCode`.** `INTERRUPTED` —
|
||||
уровень попытки (закрытие до нормального завершения); `DEADLINE` — уровень
|
||||
шага (истечение ожидания), попадает в `errorCode` попытки как первая ошибка
|
||||
секции по существующему precedence. Коды ошибок шагов остаются в
|
||||
`progress.sections[].errorCode`. Поле — открытое строковое: форма ответа и
|
||||
enum `TbankErrorCode` не расширяются (маркеры жизненного цикла — не ошибки
|
||||
T-Bank).
|
||||
|
||||
7. **Durable finish в дочерних финализаторах.** Внешний catch финализаторов
|
||||
(enrichment, operations drain/step) обязан перед возвратом или пробросом
|
||||
записать терминальный неуспешный результат своей строки шага существующим
|
||||
путём записи ошибки, а не только очистить память. Иначе шаг остаётся
|
||||
`running`, и попытка не проходит terminal barrier.
|
||||
|
||||
## Cross-cutting Implementation
|
||||
|
||||
- Подавление ошибки `settleHourly` (`.catch(() => undefined)`) допустимо только
|
||||
как пара к решению 4: `walk` удаляет attemptId из `active` в `finally`, и
|
||||
следующий tick закрывает строку recovery. Ошибка фиксируется существующим
|
||||
структурным логом.
|
||||
- Новых фоновых таймеров, очередей и таблиц нет; concurrency не меняется
|
||||
(один walk на подключение).
|
||||
|
||||
## Data / Storage Changes
|
||||
|
||||
- Миграции нет: `children`/`attempts` живут в существующем JSON `progress`
|
||||
строки `tbankSyncWork`; поле `errorCode` уже в схеме
|
||||
(`20260924170000_persist_tbank_data/migration.sql:177`).
|
||||
- Legacy-застрявшие `running` строки закрываются первым recovery-проходом
|
||||
(итог из детей + `INTERRUPTED`); чтение уже закрытых строк не меняется.
|
||||
|
||||
## Contract Implementation
|
||||
|
||||
- HTTP contract не меняется: форма `meta` и словарь состояний
|
||||
(`running`/`succeeded`/`partial`/`failed`) прежние; `cancelled` остаётся в
|
||||
enum DTO как толерантное чтение.
|
||||
- До закрытия change: backend-generated OpenAPI проверяется на семантическую
|
||||
совместимость с текущим контрактом — изменения формы не ожидаются; новые
|
||||
значения существуют только внутри открытого строкового поля диагностики и
|
||||
в hourly-read наружу не попадают.
|
||||
|
||||
## Developer Verification Strategy
|
||||
|
||||
- Изолированные SQLite-тесты reservation/coordinator (не stub
|
||||
`upcoming-events`): ветки `first`, `not-due`, `running`, `restart`,
|
||||
`partial`; итог проверяется после terminal barrier.
|
||||
- Инъекция ошибок отдельно в дочерние шаги и в родительский settle;
|
||||
восстановление после ошибки сохранения; защита от overlap и от поздней
|
||||
публикации; controlled stop/restart — нет бесконечного drain, после
|
||||
остановки новые reservations не создаются.
|
||||
- Управляемые часы — jest fake timers.
|
||||
- Сохранить существующие наборы auth/money/publication; полный
|
||||
`backend verify` на финальном diff.
|
||||
- Graceful drain не добавляется, пока тест остановки не покажет
|
||||
необходимость.
|
||||
|
||||
## UI/UX Package
|
||||
|
||||
`N/A` — UI не меняется; новый mockup не требуется.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Derivation зависит от полноты `children`: пропуск записи id при dispatch
|
||||
делает шаг невидимым для итога. Митигируется решением 2 (запись до await) и
|
||||
тестом инъекции ошибки в dispatch.
|
||||
- Per-step deadline может закрыть попытку как `partial` при медленном, но
|
||||
живом шаге; поздний результат сохраняется строкой шага, данные не теряются.
|
||||
- In-memory `active` не переживает рестарт — этот случай закрывает
|
||||
restart-ветка reservation (решение 5).
|
||||
- Константа 15 минут без конфигурации: потолок опыта одного процесса
|
||||
(`ponytail:` вынести в конфиг, если медленные шаги станут регулярными).
|
||||
|
||||
## Migration / Rollout / Rollback
|
||||
|
||||
- Rollout: один PR в backend; миграций и флагов нет; следующий tick после
|
||||
деплоя закрывает застрявшие строки.
|
||||
- Rollback contract: `N/A` — нет миграции, security или public contract
|
||||
изменения; откат — revert кода, уже закрытые recovery строки остаются в
|
||||
корректном терминальном виде.
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
Обязателен (concurrency/recovery). Варианты:
|
||||
|
||||
- (a) Root-cause: settle выводит итог из durable-детей; recovery на
|
||||
существующем tick/reservation; один guard в общей точке.
|
||||
- (b) Минимальный патч: настоящий failures-map в catch плюс guard в каждом
|
||||
вызывающем.
|
||||
- (c) Отдельный janitor/очередь с lease.
|
||||
|
||||
Выбор — (a): закрывает все error-пути одной точкой (exception, lost settle,
|
||||
краш, deadline), переиспользует существующие fence и `progress`-паттерн, без
|
||||
новых компонентов. (b) оставляет открытыми lost-settle и краш — каждый новый
|
||||
error-путь снова требует своего guard, что и есть текущий баг. (c) отклонён:
|
||||
issue явно запрещает параллельный scheduler, lease не даёт выгоды в
|
||||
одно-процессной топологии, а стоимость нового компонента выше фикса.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Documentation Impact
|
||||
|
||||
## Summary
|
||||
|
||||
Durable docs update нужен ограниченно: наблюдаемая семантика итога часовой
|
||||
попытки уже описана в AS-IS, но правила вывода итога, ограниченного ожидания и
|
||||
восстановления после сбоя сохранения/перезапуска в docs отсутствуют. Переносится
|
||||
только это знание; migration history остаётся в archived change.
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|
||||
|---|---|---|---|---|---|
|
||||
| update | `docs/architecture/cross-cutting/data-freshness.md` | cross-cutting | Рядом со словарём `lastAttempt` (`none`, `running`, `succeeded`, `partial`, `failed`): итог попытки выводится из фактических результатов её шагов; попытка, не завершившая обязательный шаг, не обозначается полным успехом; ожидание шага ограничено, зависший шаг учитывается как проблемный; попытка, потерявшая выполнение (сбой сохранения, остановка процесса), закрывается частичным успехом либо неуспехом и не блокирует следующий обход дольше обычного срока | System Analysis — Data and State Semantics, Error and Recovery Semantics; Design 1–5 | Страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta` |
|
||||
| update | `docs/integrations/tbank.md` | integration | В разделе «Состояние и ошибки»: диагностические причины жизненного цикла попытки (`INTERRUPTED` — прервана до нормального завершения, `DEADLINE` — истечение ожидания шага) — внутренняя диагностика, не ошибки T-Bank; в hourly-read наружу не попадают, начатые обращения к T-Bank не отменяются | System Analysis — Observability Semantics; Design 3, 6 | Раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла |
|
||||
|
||||
PR реализации: WealthLedger/docs#37 (open) — обе страницы обновлены, diff
|
||||
соответствует таблице выше (задачи 6.1 и 6.2 верифицируемы по нему).
|
||||
|
||||
## Diagram Promotion
|
||||
|
||||
Не переносить. Sequence-диаграммы (`hourly-attempt-terminal-barrier.mmd`,
|
||||
`hourly-bounded-wait.mmd`, `hourly-recovery.mmd`) — planning evidence этого
|
||||
change; AS-IS достаточно текстового описания семантики в `data-freshness.md`.
|
||||
|
||||
## ADR Decision
|
||||
|
||||
`N/A` — архитектурно значимого решения с новыми alternatives/consequences нет:
|
||||
чинятся существующие владельцы строк в прежней одно-процессной топологии,
|
||||
без новых компонентов и контрактов (Solution Assessment остаётся в archived
|
||||
change).
|
||||
|
||||
## Glossary / Navigation Impact
|
||||
|
||||
Нет: новые термины (`terminal barrier`, преемник попытки) — внутренние;
|
||||
публичный словарь состояний не расширяется, navigation не меняется.
|
||||
|
||||
## No-Change Rationale
|
||||
|
||||
`docs/architecture/containers.md` не меняется: раздел «Сохранение и
|
||||
восстановление данных» описывает backup/restore уровня базы, а не восстановление
|
||||
попытки приложением. Остальные AS-IS страницы не затрагиваются: HTTP contract
|
||||
и словарь состояний не изменились.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Почасовая синхронизация T-Bank может завершиться в неконсистентном состоянии
|
||||
после ошибки сохранения: незавершённая попытка записывается как полный успех,
|
||||
ошибка финализации либо подавленная ошибка закрытия оставляет попытку в
|
||||
состоянии выполнения навсегда, а локальная reservation не позволяет запустить
|
||||
следующий обход в том же процессе. Пользователь видит ложный успех и перестаёт
|
||||
получать обновления до перезапуска backend, а неудавшееся ожидание дочерних
|
||||
шагов может ждать без ограничения. Механизм подтверждён статически (BE-002,
|
||||
BE-015, RISK-001, RISK-002, TQ-003); частота проявления в production пока не
|
||||
установлена.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Итог попытки определяется фактическим результатом каждого обязательного шага:
|
||||
невыполненный шаг даёт частичный успех либо неуспех, но не полный успех, а
|
||||
уже полученные успехи отдельных счетов и разделов сохраняются.
|
||||
- Прерванная попытка (ошибка, сбой сохранения, остановка процесса, истечение
|
||||
ожидания) обозначается частичным успехом либо неуспехом и не получает нового
|
||||
публичного состояния; причина прерывания фиксируется существующим
|
||||
диагностическим полём.
|
||||
- Попытка достигает итога в ограниченное время: ожидание дочернего шага
|
||||
ограничено, а зависшая попытка не остаётся в выполнении бесконечно.
|
||||
- Состояние в SQLite восстанавливается после сбоя сохранения: брошенная попытка
|
||||
закрывается диагностируемым итогом, ожидание завершается, следующая
|
||||
допустимая попытка может стартовать без перезапуска процесса.
|
||||
- Восстановление не создаёт перекрытия обходов и не принимает устаревшие
|
||||
результаты: поздняя публикация относительно актуального преемника
|
||||
отклоняется.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Итоговый статус попытки часового обхода и обработка ошибок в дочерних
|
||||
финализаторах.
|
||||
- Ограниченное ожидание дочерних шагов и восстановление после ошибок
|
||||
завершения шага, публикации результата и закрытия попытки.
|
||||
- Защита от перекрытия обходов и от поздней публикации устаревших данных при
|
||||
восстановлении.
|
||||
- Контролируемая остановка и перезапуск процесса и их влияние на незавершённую
|
||||
попытку, подтверждённые тестами.
|
||||
- Наблюдаемый результат попытки: время начала, состояние, безопасный перечень
|
||||
проблемных счетов и разделов.
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
- Распределённая очередь, lease, поддержка нескольких реплик и изменение
|
||||
топологии deployment.
|
||||
- Удаление данных.
|
||||
- Ослабление существующих ограничений CAS для попытки, ревизии подключения и
|
||||
источника данных.
|
||||
- Новый публичный статус попытки и изменения HTTP contract.
|
||||
- Новый параллельный scheduler или замена владельцев строк синхронизации.
|
||||
- Проверка часовой cadence и cleanup workflow — независимые задачи.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- Нет.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `hourly-investment-sync`: итог попытки из фактических результатов шагов,
|
||||
ограниченное ожидание шага, диагностируемое закрытие брошенной попытки,
|
||||
отсутствие перекрытий и устаревшей публикации.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Пользователь**: достоверный статус последнего обхода вместо ложного успеха
|
||||
и бесконечного «выполняется» после сбоя сохранения; обновления продолжаются
|
||||
без перезапуска процесса.
|
||||
- **Backend**: координатор часового обхода, дочерние финализаторы обновления
|
||||
данных и reservation попытки в SQLite.
|
||||
- **Frontend**: не затрагивается — наблюдаемый словарь состояний и форма
|
||||
ответов не меняются.
|
||||
- **Внешняя система T-Bank**: contract не меняется; начатые запросы к T-Bank
|
||||
могут завершаться поздно и должны корректно отклоняться как устаревшие.
|
||||
- **Процессы**: диагностика причин прерывания попыток и восстановление после
|
||||
ошибок сохранения.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Попытка с невыполненным обязательным шагом никогда не наблюдается как полный
|
||||
успех; частичные успехи счетов и разделов остаются видимыми.
|
||||
- После однократной ошибки завершения шага, публикации результата либо закрытия
|
||||
попытки и последующего восстановления ожидание завершается, а следующая
|
||||
допустимая попытка стартует.
|
||||
- Устаревшая попытка наблюдается относительно актуального преемника; поздняя
|
||||
публикация старого процесса отклоняется и не меняет данные.
|
||||
- Координатор различает состояния `first`, `not-due`, `running`, `restart` и
|
||||
`partial`; итог проверяется после terminal barrier.
|
||||
- Сценарий остановки и перезапуска доказан тестами: нет бесконечного ожидания,
|
||||
после остановки новые попытки не создаются.
|
||||
- Сохранены существующие наборы проверок auth/money/publication, полный
|
||||
`backend verify` на финальном изменении зелёный.
|
||||
|
||||
## Assumptions / Dependencies
|
||||
|
||||
- Топология остаётся одно-процессной; межпроцессный lease и мультреплики не
|
||||
требуются.
|
||||
- Истечение ожидания не отменяет уже начатый запрос к T-Bank; защита
|
||||
опирается на существующие ограничения CAS и правило срока следующего обхода.
|
||||
- Причина прерывания фиксируется существующим диагностическим полём, поэтому
|
||||
HTTP contract и публичный словарь состояний не меняются; упомянутое в задаче
|
||||
состояние `interrupted` реализуется как частичный успех либо неуспех.
|
||||
- Graceful drain добавляется только если тест контролируемой остановки докажет
|
||||
такую необходимость.
|
||||
- Правило устаревшей публикации относится к итогу попытки и данным её шагов;
|
||||
существующие требования `tbank-data-sync` уже запрещают выдавать неполный или
|
||||
продолжающийся результат за полный, поэтому их изменение не планируется.
|
||||
@@ -0,0 +1,90 @@
|
||||
# QA Analysis
|
||||
|
||||
## Verification Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Дельта `hourly-investment-sync`: «Последний результат обхода доступен только
|
||||
для наблюдения» (MODIFIED) и ADDED «Каждая попытка обхода достигает итога в
|
||||
ограниченное время», «Восстановление после сбоя сохранения не блокирует
|
||||
последующие обходы», «Устаревшая публикация не влияет на актуальную попытку».
|
||||
- Системные проверки через resource GET `meta` и durable-состояние попытки
|
||||
после контролируемых действий backend (инъекции сбоев, остановка/запуск
|
||||
процесса).
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Внутренние unit/component тесты — developer-owned, определены Design.
|
||||
- Неизменяемые поведения `tbank-data-sync`/`tbank-connection` — только
|
||||
regression.
|
||||
- Проверка часовой cadence (T03) и cleanup workflow — независимые задачи.
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
| Requirement / Scenario | Системная проверка |
|
||||
| --- | --- |
|
||||
| Последний результат… — Сбой после частичного обновления | Инъекция сбоя шага после успешного обновления Portfolio одного счёта: GET обозначает частичный успех с проблемным шагом, данные первого счёта остаются обновлёнными |
|
||||
| Последний результат… — Сбой до первого обновления | Инъекция сбоя до завершения любого шага: GET обозначает неуспех, не полный успех |
|
||||
| Последний результат… — Частичный успех / Наблюдение не запускает обход / Старая граница подключения | Regression существующего поведения `meta` |
|
||||
| Каждая попытка… — Шаг не завершается | Зависший шаг с управляемыми часами: попытка достигает терминального итога в ограниченное время, шаг учтён как проблемный |
|
||||
| Каждая попытка… — Наблюдение не ждёт без ограничения | Наблюдение результата получает итоговое состояние, а не бесконечное «выполняется» |
|
||||
| Восстановление… — Потерянное закрытие попытки | Ошибка закрытия при живом процессе: следующий проход закрывает попытку диагностируемо, следующая допустимая попытка стартует |
|
||||
| Восстановление… — Перезапуск процесса | Остановка процесса во время обхода и повторный запуск: брошенная попытка закрыта, новый обход не ранее следующего часового срока, перекрытия нет |
|
||||
| Устаревшая публикация… — После появления преемника | Позднее завершение шага старой попытки после запуска следующей: данные не изменились, итог актуальной попытки не изменился |
|
||||
| Устаревшая публикация… — До фиксации итога | Позднее завершение шага до фиксации итога попытки: результат сохранён и учтён в итоге |
|
||||
|
||||
## Cross-system Verification
|
||||
|
||||
- T-Bank upstream не отменяется при истечении ожидания; поздний ответ после
|
||||
появления преемника не изменяет данные (проверяется на симулированном
|
||||
upstream в контролируемой среде backend-тестов).
|
||||
- Внешние ошибки шагов отображаются только безопасным перечнем проблемных
|
||||
счетов/разделов; причины `INTERRUPTED`/`DEADLINE` не раскрывают наружу
|
||||
ничего сверх существующей формы `meta`.
|
||||
|
||||
## Risk-based Priorities
|
||||
|
||||
- **Critical**: ложный полный успех (целостность данных), бесконечное
|
||||
«выполняется», перекрытие обходов, порча данных устаревшей публикацией.
|
||||
- **Compatibility**: публичный словарь состояний `meta` неизменен;
|
||||
`cancelled` в enum остаётся толерантным значением.
|
||||
- **Money/data integrity**: успешные результаты шагов не теряются при сбоях
|
||||
остальных.
|
||||
|
||||
## Test Data and Environment Needs
|
||||
|
||||
- Изолированный backend на SQLite с управляемыми часами и симулированным
|
||||
upstream T-Bank (контролируемая тестовая среда; внутренние mocks — на
|
||||
усмотрение разработчиков по Design).
|
||||
- Подключение с не менее чем двумя доступными счетами для сценариев частичного
|
||||
успеха.
|
||||
|
||||
## Manual / Exploratory Scope
|
||||
|
||||
- Ручная проверка отображения результата последнего обхода на инвестиционных
|
||||
экранах после симулированного сбоя сохранения.
|
||||
- Exploratory: повторные остановки процесса в разных фазах обхода; наблюдение
|
||||
результата сразу после старта процесса.
|
||||
|
||||
## Automation Candidates
|
||||
|
||||
- Black-box API-кандидаты (GET `meta` после инъекций сбоев, контролируемая
|
||||
остановка) — `deferred until QA repo exists`.
|
||||
|
||||
## Observability Verification
|
||||
|
||||
- Причины `INTERRUPTED`/`DEADLINE` наблюдаются в durable-диагностике попытки,
|
||||
а не в публичном API; подавленная ошибка закрытия фиксируется структурным
|
||||
логом backend.
|
||||
|
||||
## Regression Impact
|
||||
|
||||
- Существующие наборы auth/money/publication и полный `backend verify`.
|
||||
- Happy path часового обхода, первый обход после подключения, скрытие
|
||||
результата прежней границы подключения.
|
||||
|
||||
## Gaps / Blockers
|
||||
|
||||
- Gaps нет. Graceful drain остаётся условным: добавляется только если
|
||||
controlled-stop тест докажет необходимость (routing: пересмотр Design/QA
|
||||
после теста).
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Последний результат обхода доступен только для наблюдения
|
||||
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
|
||||
|
||||
#### Scenario: Частичный успех
|
||||
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
|
||||
- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт
|
||||
|
||||
#### Scenario: Наблюдение не запускает обход
|
||||
- **WHEN** User запрашивает результат последнего обхода
|
||||
- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Старая граница подключения
|
||||
- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода
|
||||
- **THEN** система не раскрывает результат прежней границы подключения
|
||||
|
||||
#### Scenario: Сбой после частичного обновления
|
||||
- **WHEN** попытка прервана ошибкой либо сбоем сохранения после обновления Portfolio одного доступного счёта
|
||||
- **THEN** результат попытки обозначает частичный успех с проблемными шагами, а не полный успех, а обновлённый Portfolio сохраняется
|
||||
|
||||
#### Scenario: Сбой до первого обновления
|
||||
- **WHEN** попытка прервана до обновления любого обязательного шага
|
||||
- **THEN** результат попытки обозначает неуспех, а не полный успех
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Каждая попытка обхода достигает итога в ограниченное время
|
||||
Каждая начатая попытка часового обхода MUST достигать итогового состояния в ограниченное время. Ожидание завершения каждого обязательного шага MUST быть ограничено; шаг, не завершившийся в это ограничение, MUST быть учтён как проблемный, а попытка MUST быть закрыта частичным успехом либо неуспехом после того, как каждый обязательный шаг достиг терминального результата либо был учтён как проблемный. Истечение ограничения MUST NOT отменять уже начатые шаги.
|
||||
|
||||
#### Scenario: Шаг не завершается
|
||||
- **WHEN** обязательный шаг не покидает состояние выполнения дольше ограничения ожидания
|
||||
- **THEN** шаг учитывается как проблемный, попытка закрывается частичным успехом либо неуспехом, а её состояние перестаёт быть выполняющимся
|
||||
|
||||
#### Scenario: Наблюдение не ждёт без ограничения
|
||||
- **WHEN** попытка не может завершиться из-за незавершённых шагов
|
||||
- **THEN** наблюдение результата получает итоговое состояние в ограниченное время и не ожидает завершения бесконечно
|
||||
|
||||
### Requirement: Восстановление после сбоя сохранения не блокирует последующие обходы
|
||||
Если попытка осталась в состоянии выполнения после сбоя завершения шага, публикации результата либо закрытия попытки, система MUST привести её к диагностируемому итоговому состоянию: частичный успех либо неуспех с сохранёнными результатами шагов, а не полный успех. Последующий обход MUST становиться допустимым по обычному правилу срока, отсчитанному от начала этой попытки. Восстановление MUST NOT создавать перекрывающийся обход и MUST NOT повторять уже полученные результаты шагов.
|
||||
|
||||
#### Scenario: Потерянное закрытие попытки
|
||||
- **WHEN** закрытие попытки не сохранилось, а процесс продолжает работать
|
||||
- **THEN** система закрывает попытку итогом из фактических результатов шагов, наблюдение получает итоговое состояние, а следующая допустимая попытка может стартовать
|
||||
|
||||
#### Scenario: Перезапуск процесса
|
||||
- **WHEN** процесс остановился после начала попытки и запускается снова
|
||||
- **THEN** брошенная попытка обозначается частичным успехом либо неуспехом с сохранёнными результатами шагов, а новый обход начинается не ранее следующего часового срока от её начала
|
||||
|
||||
### Requirement: Устаревшая публикация не влияет на актуальную попытку
|
||||
Результат шага попытки, у которой появился преемник, MUST отклоняться как устаревший: он MUST NOT обновлять данные и MUST NOT учитываться в итоге актуальной попытки. Шаг устаревшей попытки MUST наблюдаться как устаревший относительно актуального преемника. Результат шага текущей попытки, полученный до фиксации её итога, MUST сохраняться и учитываться в этом итоге независимо от того, наступил ли он до или после истечения ограничения ожидания.
|
||||
|
||||
#### Scenario: Поздняя публикация после появления преемника
|
||||
- **WHEN** шаг старой попытки завершается после запуска следующей попытки
|
||||
- **THEN** его результат отклоняется как устаревший, данные не обновляются, а итог актуальной попытки не меняется
|
||||
|
||||
#### Scenario: Поздняя публикация до фиксации итога
|
||||
- **WHEN** шаг завершился после истечения ограничения ожидания, но итог его попытки ещё не зафиксирован
|
||||
- **THEN** его результат сохраняется и учитывается при выводе итога этой попытки
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# System Analysis
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
| Requirement / Scenario | Flow | Contract / Semantics |
|
||||
| --- | --- | --- |
|
||||
| Последний результат обхода… — Сбой после частичного обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation, terminal barrier |
|
||||
| Последний результат обхода… — Сбой до первого обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation |
|
||||
| Последний результат обхода… — Частичный успех | `hourly-attempt-terminal-barrier.mmd` | Step results preserved |
|
||||
| Каждая попытка обхода достигает итога в ограниченное время — Шаг не завершается | `hourly-bounded-wait.mmd` | Bounded wait per required step |
|
||||
| Каждая попытка обхода достигает итога в ограниченное время — Наблюдение не ждёт без ограничения | `hourly-bounded-wait.mmd` | Bounded terminal outcome |
|
||||
| Восстановление после сбоя сохранения… — Потерянное закрытие попытки | `hourly-recovery.mmd` | Recovery of lost settle |
|
||||
| Восстановление после сбоя сохранения… — Перезапуск процесса | `hourly-recovery.mmd` | Recovery of abandoned attempt |
|
||||
| Устаревшая публикация… — Поздняя публикация после появления преемника | `hourly-recovery.mmd` | Successor fencing |
|
||||
| Устаревшая публикация… — Поздняя публикация до фиксации итога | `hourly-bounded-wait.mmd` | Late publication counts before outcome is recorded |
|
||||
|
||||
## Systems and Responsibilities
|
||||
|
||||
- **Hourly walk coordinator** (backend): резервирует попытку часового обхода для
|
||||
текущего подключения, запускает обязательные шаги, ждёт их терминального
|
||||
результата с ограничением, выводит итог попытки и закрывает её.
|
||||
- **Durable attempt status** (SQLite): единственная durable-запись попытки
|
||||
(состояние, диагностическая причина, результаты шагов) и источник итога для
|
||||
наблюдения и восстановления.
|
||||
- **Child step owners**: шаги membership, Portfolio, операций и будущих событий
|
||||
публикуют свой полный результат в durable-хранилище и обозначают свой
|
||||
терминальный результат; финализатор шага обязан фиксировать результат даже
|
||||
при ошибке.
|
||||
- **T-Bank и публичные источники**: внешние данные; начатые обращения не
|
||||
отменяются по внутреннему ограничению ожидания.
|
||||
- **Resource GET observer**: читает сохранённые данные и `meta` с состоянием
|
||||
последней попытки; наблюдение не запускает обход.
|
||||
|
||||
## Interaction Flows
|
||||
|
||||
- `sequences/hourly-attempt-terminal-barrier.mmd` — попытка, шаги, terminal
|
||||
barrier и вывод итога из фактических результатов шагов.
|
||||
- `sequences/hourly-bounded-wait.mmd` — ограничение ожидания шага, закрытие
|
||||
попытки по его истечении и учёт позднего результата до появления преемника.
|
||||
- `sequences/hourly-recovery.mmd` — восстановление после сбоя сохранения и
|
||||
после перезапуска процесса, отклонение устаревшей публикации.
|
||||
|
||||
## Contracts
|
||||
|
||||
- Новых и изменённых межсистемных HTTP/event contracts нет. Публичный словарь
|
||||
состояний последней попытки (`выполнение`, `полный успех`, `частичный успех`,
|
||||
`неуспех`) и форма `{ data, meta }` не меняются.
|
||||
- Машинно-читаемый target contract в `system-analysis/contracts/` не создаётся:
|
||||
изменения ограничены уточнением значений внутри существующих полей.
|
||||
- Диагностическое поле причины остаётся открытым строковым полем; значения
|
||||
причин прерывания (`INTERRUPTED` — ошибка, сбой сохранения, остановка
|
||||
процесса; `DEADLINE` — истечение ограничения ожидания шага) дополняют
|
||||
существующие коды ошибок шагов и не требуют изменения формы ответа.
|
||||
- Compatibility check до закрытия change: backend-generated OpenAPI остаётся
|
||||
семантически совместимым с текущим контрактом (ожидаются только дополнительные
|
||||
значения открытого строкового поля диагностики).
|
||||
|
||||
## Data and State Semantics
|
||||
|
||||
- **Попытка**: `running` → терминальный итог (`succeeded` | `partial` | `failed`).
|
||||
Попытка, не завершившая обязательный шаг, не может получить `succeeded`.
|
||||
- **Terminal barrier**: итог попытки выводится только после того, как каждый
|
||||
обязательный шаг достиг терминального результата либо учтён как истёкший по
|
||||
ограничению ожидания. Итог не вычисляется по неполному списку шагов.
|
||||
- **Вывод итога**: `succeeded` — только когда все обязательные шаги успешны и
|
||||
координатор сам зафиксировал закрытие попытки; `partial` — успешны некоторые
|
||||
шаги; `failed` — не успешен ни один. Успешные результаты шагов сохраняются
|
||||
независимо от итога.
|
||||
- **Причина прерывания**: попытка, закрытая до нормального завершения всех
|
||||
шагов, сохраняет причину прерывания в диагностическом поле и не превышает
|
||||
`partial`.
|
||||
- **Internal coordinator decisions** `first`, `not-due`, `running`, `restart`,
|
||||
`partial`: внутренние метки решения о запуске и проверки итога после terminal
|
||||
barrier; в HTTP contract не входят и публично не наблюдаемы.
|
||||
- **Шаги**: каждый шаг владеет своей durable-строкой результата; строка шага
|
||||
устаревшей попытки помечается устаревшей относительно преемника.
|
||||
|
||||
## Error and Recovery Semantics
|
||||
|
||||
- **Сбой шага** (внешняя ошибка T-Bank): шаг фиксирует терминальный неуспех со
|
||||
своим кодом ошибки; остальные шаги продолжаются; итог — `partial` или `failed`.
|
||||
- **Сбой сохранения** (завершение шага, публикация результата, закрытие
|
||||
попытки): ошибка не подавляется молча в durable-состоянии; попытка, оставшаяся
|
||||
`running`, закрывается восстановлением по фактическим результатам шагов.
|
||||
- **Остановка процесса**: начатая попытка остаётся `running` в SQLite;
|
||||
следующий запуск процесса закрывает её как прерванную (`partial` или `failed`
|
||||
с сохранёнными результатами шагов) и не создаёт перекрывающийся обход.
|
||||
- **Ограничение ожидания**: истечение ограничивает только ожидание шага; уже
|
||||
начатое обращение к T-Bank не отменяется. Его поздний результат сохраняется и
|
||||
учитывается при выводе итога своей попытки, если итог ещё не зафиксирован, и
|
||||
отклоняется как устаревший после появления преемника.
|
||||
- **Повтор**: неуспешная попытка не повторяется ранее следующего часового срока,
|
||||
отсчитанного от начала этой попытки; восстановление не сокращает и не
|
||||
продлевает этот срок.
|
||||
- **Overlapping**: следующая попытка не резервируется, пока предыдущая не
|
||||
терминальна; восстановление обязано сначала закрыть предыдущую.
|
||||
|
||||
## Observability Semantics
|
||||
|
||||
- Наблюдаемый итог попытки и безопасный перечень проблемных счетов/разделов —
|
||||
существующее обязательство; значения итога не расширяются.
|
||||
- Семантические коды причин: `INTERRUPTED` — попытка прервана до нормального
|
||||
завершения (ошибка, сбой сохранения, остановка процесса); `DEADLINE` — шаг не
|
||||
завершился в ограничение ожидания. Коды сохраняются в диагностическом поле
|
||||
попытки и/или шага и дополняют коды ошибок шагов без изменения формы `meta`.
|
||||
- SDK/exporter/logger — вне границ этого изменения; используется существующий
|
||||
структурный лог backend.
|
||||
|
||||
## Security and NFR at System Boundaries
|
||||
|
||||
- Граница наблюдения попытки остаётся границей текущего подключения: результат
|
||||
прежней границы не раскрывается (существующее обязательство).
|
||||
- Ограниченное время достижения итога попытки — наблюдаемое обязательство
|
||||
надёжности: ожидание шага и ожидание наблюдателя ограничены.
|
||||
- Новых trust boundaries, authn/authz требований нет.
|
||||
|
||||
## Compatibility / Migration
|
||||
|
||||
- Схема данных и HTTP contract не меняются; миграция не требуется.
|
||||
- Уже застрявшие строки попыток в состоянии выполнения закрываются первым
|
||||
проходом восстановления по правилам этого изменения: итог из фактических
|
||||
результатов шагов, причина прерывания, без полного успеха. Это ожидаемое
|
||||
разовое приведение legacy-состояния к диагностируемому виду.
|
||||
- Совместимость с одно-процессной топологией сохраняется; межпроцессный lease
|
||||
не требуется.
|
||||
|
||||
## Durable Documentation Candidates
|
||||
|
||||
- `docs/integrations/tbank.md` (раздел «Состояние и ошибки»): итог часовой
|
||||
попытки, причины прерывания и правило ограниченного ожидания как часть
|
||||
наблюдаемого состояния интеграции.
|
||||
- `docs/architecture/containers.md` (раздел «Сохранение и восстановление
|
||||
данных»): восстановление попытки после сбоя сохранения и после перезапуска
|
||||
процесса как durable-свойство системы.
|
||||
- Итоговое решение о переносе принимает `docs-impact`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Нет блокирующих вопросов. Частота проявления механизма в production остаётся
|
||||
не установленной и не влияет на contract.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
sequenceDiagram
|
||||
participant Coordinator
|
||||
participant DB as Durable attempt status
|
||||
participant Steps as Child step owners
|
||||
participant TB as T-Bank and public sources
|
||||
participant API as Resource GET
|
||||
|
||||
Coordinator->>DB: Reserve attempt for current connection
|
||||
DB-->>Coordinator: Attempt with connection revision
|
||||
Coordinator->>Steps: Run required steps (membership, portfolios, operations, events)
|
||||
Steps->>TB: Fetch missing data
|
||||
Steps->>DB: Publish complete successful step result
|
||||
Steps->>DB: Record terminal step result (success or failure with code)
|
||||
Coordinator->>DB: Read step results after terminal barrier
|
||||
alt All required steps succeeded
|
||||
Coordinator->>DB: Settle succeeded
|
||||
else Some required steps succeeded
|
||||
Coordinator->>DB: Settle partial with safe problem sections
|
||||
else No required step succeeded
|
||||
Coordinator->>DB: Settle failed with safe problem sections
|
||||
end
|
||||
API->>DB: Read last attempt result
|
||||
DB-->>API: Time, outcome, safe problem list
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
sequenceDiagram
|
||||
participant Coordinator
|
||||
participant DB as Durable attempt status
|
||||
participant Steps as Child step owners
|
||||
participant TB as T-Bank and public sources
|
||||
|
||||
Coordinator->>Steps: Run required steps
|
||||
Steps->>TB: Start upstream call
|
||||
Coordinator->>Coordinator: Wait for step result within bound
|
||||
alt Step reaches terminal result in time
|
||||
Steps->>DB: Publish complete result
|
||||
Coordinator->>DB: Settle attempt after terminal barrier
|
||||
else Wait bound expires
|
||||
Note over Coordinator,TB: Upstream call is not cancelled
|
||||
Coordinator->>DB: Record step problem cause DEADLINE
|
||||
Coordinator->>DB: Settle partial or failed after terminal barrier
|
||||
Steps->>TB: Upstream call completes later
|
||||
Steps->>DB: Publish late result for the same attempt
|
||||
alt Outcome not recorded yet and no successor
|
||||
DB-->>Steps: Late result is preserved and counts at outcome derivation
|
||||
else Successor attempt already started
|
||||
DB-->>Steps: Publication rejected as stale
|
||||
end
|
||||
end
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
sequenceDiagram
|
||||
participant Coordinator
|
||||
participant DB as Durable attempt status
|
||||
participant Steps as Child step owners
|
||||
|
||||
Note over Coordinator,DB: Attempt is still running but its walk no longer executes
|
||||
Coordinator->>DB: Detect running attempt without active walk
|
||||
alt Process still alive after lost settle
|
||||
Coordinator->>DB: Read actual step results
|
||||
Coordinator->>DB: Close attempt as partial or failed with cause INTERRUPTED
|
||||
else Process restarted after stop
|
||||
Coordinator->>DB: Read actual step results
|
||||
Coordinator->>DB: Close abandoned attempt as partial or failed with cause INTERRUPTED
|
||||
end
|
||||
DB-->>Coordinator: Attempt is terminal, no overlap possible
|
||||
Coordinator->>DB: Reserve next attempt at next hourly term from attempt start
|
||||
alt Next term reached
|
||||
DB-->>Coordinator: Successor attempt starts
|
||||
Steps->>DB: Late publication from previous attempt
|
||||
DB-->>Steps: Publication rejected as stale relative to successor
|
||||
else Next term not reached
|
||||
DB-->>Coordinator: No work (not-due)
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Данные попытки и вывод итога
|
||||
|
||||
- [x] 1.1 [backend] `settleHourly` выводит итог попытки из durable-строк шагов
|
||||
в одной транзакции; `cause: 'INTERRUPTED'` капит итог до `partial`;
|
||||
вызывающие не передают результат и список ошибок
|
||||
- **Covers:** `hourly-investment-sync` — «Последний результат обхода доступен только для наблюдения»
|
||||
- **Source:** Design — Decisions 1, 6; System Analysis — Data and State Semantics
|
||||
- **Verification:** изолированный SQLite-тест: сбой после частичного обновления даёт `partial` с сохранённым успехом; сбой до первого шага даёт `failed`; ложный `succeeded` невозможен ни на одном error-пути
|
||||
- **Depends on:** none
|
||||
- [x] 1.2 [backend] id строк шагов персистятся в `progress` hourly-попытки
|
||||
(`children` + `attempts`) в момент dispatch, до ожидания шага
|
||||
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы», «Устаревшая публикация не влияет на актуальную попытку»
|
||||
- **Source:** Design — Decisions 2, 5
|
||||
- **Verification:** SQLite-тест: после потери выполнения попытки множество шагов восстанавливается из `progress`, включая шаг, упавший в dispatch
|
||||
- **Depends on:** 1.1
|
||||
|
||||
## 2. Ограниченное ожидание и восстановление
|
||||
|
||||
- [x] 2.1 [backend] ограничение ожидания на каждый шаг в `settledWorks`
|
||||
(константа ~15 минут, per-id deadline); истёкший шаг учитывается проблемным
|
||||
с причиной `DEADLINE`; начатые обращения к T-Bank не отменяются; часы в
|
||||
тестах — jest fake timers без инъекции clock в production-код
|
||||
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время»
|
||||
- **Source:** Design — Decision 3; System Analysis — Error and Recovery Semantics
|
||||
- **Verification:** SQLite-тест с зависшим шагом на fake timers: попытка терминальна в ограниченное время, наблюдение не ждёт бесконечно
|
||||
- **Depends on:** 1.1
|
||||
- [x] 2.2 [backend] in-memory `active`-реестр обходов + `recoverAbandoned`
|
||||
перед reservation: running-попытка своего `processRunId` без активного walk
|
||||
закрывается derived settle с `INTERRUPTED`; застрявшие legacy-строки
|
||||
закрываются первым проходом
|
||||
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Потерянное закрытие попытки)
|
||||
- **Source:** Design — Decisions 4, 6; System Analysis — Error and Recovery Semantics
|
||||
- **Verification:** SQLite-тест: подавленная ошибка закрытия при живом процессе — следующий проход закрывает попытку, следующая допустимая попытка стартует
|
||||
- **Depends on:** 1.1, 1.2
|
||||
- [x] 2.3 [backend] restart-ветка `reserveHourly` закрывает чужую running-попытку
|
||||
derived settle с `INTERRUPTED` вместо голого `cancelled`; `due`-гейт и
|
||||
защита от overlap прежние
|
||||
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Перезапуск процесса)
|
||||
- **Source:** Design — Decision 5
|
||||
- **Verification:** SQLite-тест перезапуска процесса: брошенная попытка закрыта `partial`/`failed` с сохранёнными результатами шагов, новый обход не ранее следующего часового срока, перекрытия нет
|
||||
- **Depends on:** 1.1, 1.2
|
||||
|
||||
- [x] 2.4 [backend] ожидания walk до durable-строк шагов
|
||||
(`membership.request`/`member.done`, `access.local`, dispatch
|
||||
`portfolio`/`operations`/`events.refresh`, post-wait `events.get`)
|
||||
ограничены тем же `WORK_WAIT_BOUND_MS` через race; истечение — проблемная
|
||||
секция либо `INTERRUPTED`-закрытие без отмены вызова
|
||||
(WealthLedger/backend#78)
|
||||
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время»
|
||||
- **Source:** Design — Decision 3; review specs#33 (CRITICAL)
|
||||
- **Verification:** SQLite-тесты с никогда не завершающимися dispatch/membership/`done`/`get`: попытка терминальна в ограниченное время, поздние вызовы не отменяются
|
||||
- **Depends on:** 2.1
|
||||
|
||||
## 3. Финализаторы шагов
|
||||
|
||||
- [x] 3.1 [backend] внешний catch финализаторов шагов (enrichment, operations
|
||||
drain/step) записывает терминальный неуспешный результат своей строки шага
|
||||
существующим путём записи ошибки перед возвратом или пробросом
|
||||
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время» (шаг покидает running)
|
||||
- **Source:** Design — Decision 7
|
||||
- **Verification:** SQLite-тест с инъекцией ошибки финализатора: строка шага терминальна, попытка проходит terminal barrier
|
||||
- **Depends on:** none
|
||||
|
||||
## 4. Поздняя публикация
|
||||
|
||||
- [x] 4.1 [backend] подтверждено fence-поведение публикации шага: результат до
|
||||
фиксации итога попытки сохраняется и учитывается в итоге; после появления
|
||||
преемника — отклоняется без изменения данных и итога актуальной попытки
|
||||
- **Covers:** `hourly-investment-sync` — «Устаревшая публикация не влияет на актуальную попытку»
|
||||
- **Source:** Design — Decisions 1–3; System Analysis — Error and Recovery Semantics
|
||||
- **Verification:** SQLite-тесты поздней публикации: до фиксации итога — учтён; после successor — данные и итог актуальной попытки не изменились
|
||||
- **Depends on:** 1.1, 1.2, 2.1
|
||||
|
||||
## 5. Системная верификация backend
|
||||
|
||||
- [x] 5.1 [backend] полный набор изолированных SQLite-тестов coordinator:
|
||||
ветки `first`, `not-due`, `running`, `restart`, `partial`, итог после
|
||||
terminal barrier; инъекции ошибок отдельно в дочерние шаги и родительский
|
||||
coordinator; controlled stop/restart без бесконечного drain и без новых
|
||||
reservations после остановки; scheduler stub `upcoming-events` не является
|
||||
единственным доказательством
|
||||
- **Covers:** `hourly-investment-sync` — все Requirements дельты
|
||||
- **Source:** Design — Developer Verification Strategy; QA Analysis — Coverage Matrix, Risk-based Priorities
|
||||
- **Verification:** полный `backend verify` зелёный на финальном diff; наборы auth/money/publication сохранены
|
||||
- **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1
|
||||
- [x] 5.2 [backend] backend-generated OpenAPI семантически совместим с
|
||||
текущим контрактом: форма `meta` и словарь состояний не изменились, новые
|
||||
значения только внутри открытого строкового поля диагностики
|
||||
- **Covers:** `N/A` — защита контракта (изменений контракта нет)
|
||||
- **Source:** Design — Contract Implementation; System Analysis — Contracts
|
||||
- **Verification:** diff сгенерированного OpenAPI до/после изменения не содержит shape-изменений
|
||||
- **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1
|
||||
|
||||
## 6. Canonical documentation
|
||||
|
||||
- [x] 6.1 [docs] обновить `data-freshness.md`: вывод итога из фактических
|
||||
результатов шагов, ограниченное ожидание шага, закрытие брошенной попытки
|
||||
без блокировки расписания — рядом со словарём `lastAttempt`
|
||||
- **Covers:** `N/A` — docs promotion
|
||||
- **Source:** docs-impact — Documentation Changes, строка 1
|
||||
- **Verification:** страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta`
|
||||
- **Depends on:** 5.1
|
||||
- [x] 6.2 [docs] обновить раздел «Состояние и ошибки» `tbank.md`: причины
|
||||
жизненного цикла `INTERRUPTED`/`DEADLINE` — внутренняя диагностика, не
|
||||
ошибки T-Bank; начатые обращения не отменяются
|
||||
- **Covers:** `N/A` — docs promotion
|
||||
- **Source:** docs-impact — Documentation Changes, строка 2
|
||||
- **Verification:** раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла
|
||||
- **Depends on:** 5.1
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd-v2
|
||||
created: 2026-10-14
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user