chore(release): 0.1.4 #35
@@ -1,12 +1,17 @@
|
||||
# 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 применяется по явному запросу согласно [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 эту переходную заметку можно удалить отдельным изменением.
|
||||
|
||||
## Проверка
|
||||
|
||||
@@ -14,11 +19,16 @@ Design должен оценить `frontend`, `backend`, `specs`, `docs` в Del
|
||||
|
||||
```bash
|
||||
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 не закреплена отдельным файлом: при обновлении проверяйте обе
|
||||
schema, затронутые changes и canonical specs, фиксируя `openspec --version` вместе
|
||||
с результатами проверок. Structural validation не заменяет semantic verification
|
||||
и independent read-only review перед archive.
|
||||
|
||||
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
|
||||
`npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1.10.0
|
||||
@@ -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-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
|
||||
+67
-7
@@ -1,9 +1,69 @@
|
||||
schema: wealth-ledger-sdd
|
||||
schema: wealth-ledger-sdd-v2
|
||||
|
||||
context: |
|
||||
WealthLedger состоит из независимых frontend, backend, specs и docs Git-репозиториев.
|
||||
Canonical OpenSpec authority — store wealth-ledger-specs в локальном openspec/.
|
||||
Инструкции и validation specs находятся в этом репозитории; соседние checkout не требуются.
|
||||
Локальная разработка и CI используют Node из собственной .nvmrc; порядок работы задаёт AGENTS.md.
|
||||
Project artifacts объясняют решения по-русски; exact OpenSpec headings, tokens,
|
||||
commands и normative keywords SHALL/MUST сохраняются на английском.
|
||||
WealthLedger — multi-repository workspace. The `main` repository is the workspace
|
||||
root and contains sibling repositories `frontend/`, `backend/`, `docs/` and `specs/`.
|
||||
OpenSpec configuration, schemas, changes and canonical behavior specs live only in
|
||||
`specs/openspec/` and are accessed as store `wealth-ledger-specs`. There is no
|
||||
`openspec/` directory or `config.yaml` in `main`, `frontend`, `backend` or `docs`.
|
||||
When an OpenSpec command is invoked from the workspace root, target the canonical
|
||||
store explicitly when that command supports store selection.
|
||||
New changes use `wealth-ledger-sdd-v2`; active changes explicitly pinned to
|
||||
`wealth-ledger-sdd` keep their original proposal → specs → design → tasks → apply
|
||||
lifecycle and existing approvals until completion. Never infer approval from
|
||||
the existence of an artifact. Schema selection comes from each change's
|
||||
`.openspec.yaml`, not from the current default.
|
||||
|
||||
Repository responsibilities and sources of truth:
|
||||
- `main` (`./`): workspace instructions, shared configuration, development Compose
|
||||
and release/workspace orchestration. It MUST NOT contain product feature code.
|
||||
- `frontend/`: React/Vite product implementation.
|
||||
- `backend/`: NestJS product API, Prisma data model and SQLite persistence.
|
||||
- `docs/`: canonical AS-IS product and architecture knowledge. Current architecture,
|
||||
integrations and durable diagrams describe the system as it exists now; historical
|
||||
architectural rationale is preserved in ADRs and archived changes.
|
||||
- `specs/`: OpenSpec changes plus canonical behavior specs. Canonical specs contain
|
||||
normative observable behavior only, not durable implementation documentation.
|
||||
- Future `qa/`: Playwright/E2E automation when the repository actually exists.
|
||||
|
||||
Contract authority:
|
||||
- For v2 changes, new or changed cross-system contracts are planned contract-first
|
||||
in the change's `system-analysis/contracts/` before implementation.
|
||||
- After implementation, backend-generated OpenAPI is the executable/current HTTP API
|
||||
contract and MUST remain semantically compatible with the approved target contract.
|
||||
- Do not create a second permanent machine-readable API source of truth in canonical
|
||||
OpenSpec specs or docs.
|
||||
|
||||
Documentation authority:
|
||||
- `docs/architecture/`, `docs/domains/`, `docs/integrations/` and related pages hold
|
||||
durable AS-IS explanations.
|
||||
- V2 changes select durable knowledge through approved `docs-impact`; legacy
|
||||
changes continue their approved delivery map. Change-specific reasoning,
|
||||
migration history and rejected options remain in the archived OpenSpec change
|
||||
unless an ADR is explicitly required.
|
||||
|
||||
Language and normative terminology:
|
||||
- Human-facing project artifacts are written in Russian, while exact OpenSpec headings,
|
||||
tokens, commands, identifiers and machine-readable contract syntax remain unchanged.
|
||||
- Normative keywords follow BCP 14 (RFC 2119 + RFC 8174). `MUST`, `MUST NOT`,
|
||||
`REQUIRED`, `SHALL`, `SHALL NOT`, `SHOULD`, `SHOULD NOT`, `RECOMMENDED`,
|
||||
`NOT RECOMMENDED`, `MAY` and `OPTIONAL` have normative meaning only when written
|
||||
in uppercase. Lowercase forms have their ordinary-language meaning.
|
||||
- Do not use uppercase BCP 14 keywords merely for emphasis. Use them only when the
|
||||
statement intentionally imposes a normative obligation or permitted variation.
|
||||
|
||||
A downstream artifact cannot silently change upstream semantics: route changes
|
||||
to the earliest affected artifact and reapprove only its affected dependents.
|
||||
Native OpenSpec sync/archive owns behavior delta → canonical specs; docs updates
|
||||
follow approved delivery tasks, not an automatic copy of system analysis.
|
||||
|
||||
Repository-local `AGENTS.md` files remain authoritative for implementation conventions
|
||||
inside their repositories. Load only the affected repository instructions and relevant
|
||||
code/docs; do not scan the entire workspace without need.
|
||||
|
||||
operations:
|
||||
archive:
|
||||
guidance:
|
||||
- Keep the change's selected schema and explicit approvals; do not impose v2-only artifacts on legacy changes.
|
||||
- Complete implementation, developer checks, independent verification and canonical documentation before archive. For v2 changes, also check applicable QA Analysis, docs-impact and target contract compatibility with backend-generated OpenAPI.
|
||||
- Native OpenSpec sync/archive updates canonical behavior specs, not implementation documentation.
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
name: wealth-ledger-sdd-v2
|
||||
version: 8
|
||||
description: Multi-repo SDD workflow - proposal → specs → system-analysis → design + qa-analysis → docs-impact → tasks → apply
|
||||
|
||||
artifacts:
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
description: Owner-approved intent, scope, capabilities, impact and success criteria
|
||||
template: proposal.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Proposal фиксирует намерение изменения: зачем оно нужно, что меняется на
|
||||
уровне продукта/системы, где проходит scope и по каким наблюдаемым признакам
|
||||
change можно считать успешным. Это не requirements specification, не system
|
||||
analysis и не engineering design.
|
||||
|
||||
Read First:
|
||||
- Прочитай релевантные canonical behavior specs из `specs/openspec/specs/`.
|
||||
- Прочитай только релевантные AS-IS pages в `docs/`: `overview.md`,
|
||||
`architecture/`, `domains/`, `integrations/`, `adr/`, `design/`.
|
||||
- Не исследуй code-level реализацию глубже, чем требуется для корректного
|
||||
scope/impact. Proposal не должен зависеть от случайной структуры кода.
|
||||
|
||||
Critical Rules:
|
||||
- Один Proposal = одно связное намерение. Независимо поставляемые или
|
||||
независимо согласуемые изменения предпочитай разделять.
|
||||
- Пиши языком Change Owner: проблема, цель, scope, observable outcome.
|
||||
- Не дублируй будущие Requirements/Scenarios и не проектируй решение.
|
||||
- Новую/изменяемую интеграцию можно обозначить как impact/dependency, но
|
||||
protocol, endpoint, payload, error mapping и sequence здесь запрещены.
|
||||
- Capabilities перечисляют каждую новую или изменяемую behavior spec точным
|
||||
canonical path.
|
||||
- Если observable behavior не меняется, не выдумывай Requirement: capability
|
||||
delta остаётся пустой, а change использует `.openspec.yaml: skip_specs: true`.
|
||||
- Breaking observable behavior помечай **BREAKING**.
|
||||
- Вопрос, способный изменить intent/scope/capabilities, должен быть разрешён
|
||||
до approval Proposal.
|
||||
|
||||
Required Shape:
|
||||
- **Why**: проблема/возможность и почему ею нужно заняться сейчас.
|
||||
- **What Changes**: краткий список изменяемых outcomes.
|
||||
- **Scope**:
|
||||
- **In Scope**
|
||||
- **Out of Scope / Non-Goals**
|
||||
- **Capabilities**:
|
||||
- **New Capabilities**: exact `specs/<capability-path>/spec.md`.
|
||||
- **Modified Capabilities**: exact existing capability path только если
|
||||
меняются Requirements.
|
||||
- **Impact**: пользователи, процессы, домены, внешние системы и вероятно
|
||||
затрагиваемые areas на high level; без code/repository topology деталей.
|
||||
- **Success Criteria**: продуктовые/системные наблюдаемые признаки успеха.
|
||||
- **Assumptions / Dependencies**: только существенные для scope.
|
||||
|
||||
MUST NOT Include:
|
||||
- files/classes/functions/hooks/tables/ORM entities;
|
||||
- libraries/frameworks/storage/cache/queue/transport choices;
|
||||
- endpoint/payload/error-code contracts;
|
||||
- sequence/component diagrams;
|
||||
- unit/integration/E2E plan;
|
||||
- implementation/tasks/PR/commit/branch plan;
|
||||
- пиксельные UI details или инженерные решения.
|
||||
|
||||
Validation and Approval:
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Structural success не является semantic approval.
|
||||
- Покажи Proposal Change Owner и остановись до explicit approval.
|
||||
requires: []
|
||||
|
||||
- id: specs
|
||||
generates: "specs/**/*.md"
|
||||
description: Normative black-box behavioral deltas and acceptance-level invariants
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Delta Specs — нормативный black-box behavior contract. Они отвечают только
|
||||
на вопрос: "Что система обязана делать с точки зрения пользователя или
|
||||
внешнего наблюдателя?" После sync/archive они становятся canonical behavior
|
||||
specs в repository `specs/`.
|
||||
|
||||
Preconditions:
|
||||
- Proposal explicitly approved.
|
||||
- Используй только capabilities из approved Proposal.
|
||||
- Для existing capability прочитай canonical spec целиком.
|
||||
- При `.openspec.yaml: skip_specs: true` не создавай spec files и не
|
||||
выдумывай behavior ради workflow.
|
||||
|
||||
Critical Rules:
|
||||
- Один Requirement = одно устойчивое observable behavior/invariant.
|
||||
- Нормативный язык следует project BCP 14 policy из `config.yaml`:
|
||||
SHALL/MUST для обязательств; SHOULD/MAY только при намеренной вариативности.
|
||||
- Каждый Requirement имеет минимум один `#### Scenario:` с конкретным
|
||||
WHEN/THEN; GIVEN допустим для precondition.
|
||||
- Scenario должен быть проверяемым на уровне системы, но не предписывает
|
||||
конкретный test level/automation implementation.
|
||||
- Покрой значимые happy path, business errors, permissions, boundaries,
|
||||
state transitions и compatibility cases; не перечисляй комбинации ради
|
||||
искусственной полноты.
|
||||
- Security/privacy/reliability/performance constraints включай только если
|
||||
они являются observable/contractual obligation.
|
||||
- Технический термин допустим только когда он сам является стабильным
|
||||
внешним обязательством. Internal implementation terms запрещены.
|
||||
- Не дели Requirements на frontend/backend/main/docs/qa: spec описывает
|
||||
систему как единое целое.
|
||||
- System names, transport, endpoint, schema и code mappings по умолчанию
|
||||
принадлежат System Analysis. Они попадают в spec только если их identity
|
||||
сама является частью публичного/регуляторного behavior contract.
|
||||
|
||||
Delta Operations:
|
||||
- `ADDED`: новый Requirement или capability.
|
||||
- `MODIFIED`: скопируй ПОЛНЫЙ existing Requirement block со всеми scenarios,
|
||||
затем измени behavior; heading должен совпасть точно.
|
||||
- `REMOVED`: добавь **Reason** и **Migration**.
|
||||
- `RENAMED`: только rename heading в формате `FROM:` / `TO:`.
|
||||
- Не оставляй пустые operation sections.
|
||||
|
||||
New Capability:
|
||||
- Добавь `## Purpose` из 1–2 предложений (не менее 50 characters).
|
||||
- Existing Purpose в delta не дублируй.
|
||||
- Не переименовывай capability path обычной MODIFIED delta.
|
||||
|
||||
Quality Gate:
|
||||
- Requirement однозначен и атомарен.
|
||||
- Scenario имеет ясный trigger и проверяемый outcome.
|
||||
- Нет "работает корректно", "быстро", "удобно" без критерия.
|
||||
- Нет implementation details.
|
||||
- Нет поведения вне approved Proposal.
|
||||
- Нет противоречия canonical specs.
|
||||
- Manual QA способен понять expected behavior без чтения implementation code.
|
||||
|
||||
Feedback Routing:
|
||||
- intent/scope/capabilities → Proposal.
|
||||
- Systems/contracts/mappings/integration semantics → не решай здесь;
|
||||
детализируй позже в System Analysis.
|
||||
|
||||
Validation and Approval:
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Owner: System Analyst. QA выполняет testability review; affected engineers
|
||||
проверяют реализуемость, не подменяя requirements design.
|
||||
- Остановись до explicit Delta Specs approval.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: system-analysis
|
||||
generates: system-analysis/analysis.md
|
||||
description: Cross-system decomposition, complete flows, contract-first interfaces, mappings, failures and semantic observability
|
||||
template: system-analysis.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
System Analysis детализирует approved Delta Specs на уровне взаимодействия
|
||||
систем. Он отвечает на вопрос: "Как требуемое behavior распределяется между
|
||||
системами и какие межсистемные contracts/flows должны существовать?"
|
||||
Это артефакт системного аналитика, а не internal engineering design.
|
||||
|
||||
Read First:
|
||||
- Approved Proposal и все Delta Specs change.
|
||||
- Релевантный AS-IS из `docs/`: `architecture/`, `domains/`, `integrations/`,
|
||||
`adr/` и только нужные cross-cutting pages.
|
||||
- Для существующего HTTP API прочитай текущий backend-generated OpenAPI.
|
||||
- Для существующих внешних integration contracts прочитай их canonical/runtime
|
||||
source, если он доступен.
|
||||
- Не принимай текущий code layout за обязательный contract.
|
||||
|
||||
Critical Rules:
|
||||
- System Analysis MAY elaborate Requirements, но MUST NOT молча вводить
|
||||
новое observable behavior. Uppercase BCP 14 keywords используй только для
|
||||
реальных межсистемных обязательств, не как усилители обычной прозы. Если обнаружен behavioral gap, сначала обнови
|
||||
Delta Specs и переутверди их.
|
||||
- System-level interactions и contracts согласуются здесь ДО Design.
|
||||
- Sequence diagrams здесь — полные system-level flows: участники, calls/events
|
||||
и релевантные success/business-error/technical-failure branches.
|
||||
- Design не переизобретает согласованные здесь interactions/contracts.
|
||||
- Для сложного процесса предпочти несколько читаемых sequence diagrams одной
|
||||
огромной диаграмме.
|
||||
- Не описывай internal classes/hooks/modules/repositories/FSD/NestJS layout,
|
||||
DB tables или developer unit-test strategy.
|
||||
|
||||
Contract-First Policy:
|
||||
- Для нового/изменяемого межсистемного HTTP/event contract target contract
|
||||
определяется и согласуется ДО implementation.
|
||||
- Machine-readable target contracts храни в
|
||||
`system-analysis/contracts/` (OpenAPI/AsyncAPI/Proto/JSON Schema).
|
||||
- Markdown фиксирует semantics и ссылается на contract files, не копирует
|
||||
большие schemas.
|
||||
- Frontend и Backend могут независимо планировать/реализовывать работу против
|
||||
approved target contract.
|
||||
- WealthLedger backend-generated OpenAPI остаётся executable/runtime source
|
||||
после реализации. До закрытия change он MUST быть semantic-compatible с
|
||||
approved contract from System Analysis.
|
||||
- Не создавай второй вечный canonical OpenAPI в `specs/`: approved contract
|
||||
остаётся частью archived change. Текущее executable API подтверждается
|
||||
backend code/generated OpenAPI; `docs/` хранит durable explanation, а не
|
||||
дублированную machine-readable truth.
|
||||
- Semantic mismatch generated OpenAPI ↔ approved contract возвращает change
|
||||
в System Analysis; нельзя тихо принять реализацию как новую спецификацию.
|
||||
|
||||
Required Shape:
|
||||
- **Requirement Coverage**:
|
||||
mapping релевантных Requirements/Scenarios на flows/contracts.
|
||||
- **Systems and Responsibilities**:
|
||||
participating applications/services/external systems, role и boundary.
|
||||
- **Interaction Flows**:
|
||||
flows со ссылками на `system-analysis/sequences/*.mmd`.
|
||||
- **Contracts**:
|
||||
producer/consumer, protocol/transport, operation/event, version,
|
||||
machine-readable schema reference, compatibility expectations,
|
||||
correlation/idempotency semantics где нужны.
|
||||
- **Data and State Semantics**:
|
||||
source of truth/data ownership, cross-system mappings, state transitions,
|
||||
validation boundaries, consistency expectations.
|
||||
- **Error and Recovery Semantics**:
|
||||
business vs technical failures, external→domain mapping, timeout,
|
||||
duplicate/out-of-order/callback semantics и retry только на boundary level.
|
||||
- **Observability Semantics**:
|
||||
какие значимые product/business/system events обязаны фиксироваться.
|
||||
Допустимы semantic mappings вроде `code=1 → NO_MONEY`,
|
||||
`code=2 → FRAUD`, `Pay click → BUTTON_CLICK`, required attributes/triggers.
|
||||
SDK/exporter/logger/instrument — Design.
|
||||
- **Security and NFR at System Boundaries**:
|
||||
authn/authz/trust boundaries, SLA/latency/rate/compatibility constraints,
|
||||
только если они являются system contract.
|
||||
- **Compatibility / Migration**:
|
||||
system-level transition (например SOAP → REST), coexistence/cutover,
|
||||
backward compatibility и dependency sequencing.
|
||||
- **Durable Documentation Candidates**:
|
||||
перечисли только факты/диаграммы/решения, которые вероятно должны остаться
|
||||
в AS-IS `docs/` после завершения change. Финальное решение принимает
|
||||
`docs-impact` после Engineering Design.
|
||||
- **Open Questions**:
|
||||
только genuinely non-blocking. Вопрос, меняющий behavior/contract/
|
||||
implementability, блокирует approval.
|
||||
|
||||
MUST NOT Include:
|
||||
- internal implementation files/classes/framework choices;
|
||||
- internal DB schema/migration details;
|
||||
- developer unit/component/internal integration test prescriptions;
|
||||
- task checklist;
|
||||
- новое behavior вне Delta Specs;
|
||||
- попытку заранее переписать `docs/`: этот artifact описывает TO-BE change,
|
||||
а promotion в canonical AS-IS планируется отдельно.
|
||||
|
||||
Review and Approval:
|
||||
- Owner: System Analyst.
|
||||
- Mandatory reviewers: affected Frontend/Backend engineers; QA reviews
|
||||
testability/failure semantics.
|
||||
- behavior change → Specs; intent/scope change → Proposal.
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Остановись до explicit System Analysis approval.
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: design
|
||||
generates: design.md
|
||||
description: Internal engineering design for implementing approved behavior and system contracts in main/frontend/backend
|
||||
template: design.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Design отвечает на вопрос: "Как affected implementation repositories
|
||||
реализуют свою сторону approved Specs и System Analysis?"
|
||||
Это internal engineering design, а не повторный system analysis.
|
||||
|
||||
Preconditions:
|
||||
- Proposal, applicable Specs и System Analysis approved.
|
||||
- System-level sequences/contracts/mappings считаются input constraints.
|
||||
|
||||
Critical Rules:
|
||||
- Не добавляй observable behavior вне Specs.
|
||||
- Не изменяй system contract/interaction semantics из System Analysis.
|
||||
- Технически нереализуемый contract возвращай в System Analysis, не создавай
|
||||
альтернативный private contract внутри Design.
|
||||
- Не пересказывай upstream artifacts; проектируй внутренний implementation.
|
||||
- Files/classes/modules/components допустимы только когда помогают зафиксировать
|
||||
существенное решение; Design не file-by-file checklist.
|
||||
- Developer-owned test strategy принадлежит implementation owners. QA не
|
||||
диктует unit/component/internal integration tests frontend/backend.
|
||||
- Blocking вопрос по approach/migration/task decomposition реши до approval.
|
||||
|
||||
Required Shape:
|
||||
- **Context and Constraints**: relevant current implementation constraints.
|
||||
- **Technical Goals / Non-Goals**.
|
||||
- **Implementation Impact Map**:
|
||||
рассмотри `main`, `frontend`, `backend` и future `qa`.
|
||||
Для каждой area: `affected` + outcome либо `N/A` + reason.
|
||||
`main` affected только при workspace/dev/release configuration impact.
|
||||
`qa` существует только после появления реального repository.
|
||||
- **Decisions**:
|
||||
ключевые implementation decisions, rationale, rejected alternatives.
|
||||
Разделяй по affected repositories:
|
||||
- `main`: Compose/workspace/release/config orchestration only.
|
||||
- `frontend`: React/Vite structure, state/data flow, routing, UI composition,
|
||||
API client integration, frontend developer verification approach.
|
||||
- `backend`: NestJS modules/services, Prisma/SQLite persistence, migrations,
|
||||
transactions, background work, backend developer verification approach.
|
||||
- `qa`: только когда repo существует; internal Playwright architecture
|
||||
принадлежит automation owner.
|
||||
- **Cross-cutting Implementation**:
|
||||
security implementation, feature flags, technical observability/logging,
|
||||
caching, concurrency, performance mechanisms, internal error handling.
|
||||
- **Data / Storage Changes**:
|
||||
Prisma/SQLite schema, migration/backfill/data-safety details.
|
||||
- **Contract Implementation**:
|
||||
как implementation будет соответствовать approved contract; для backend
|
||||
укажи, как generated OpenAPI будет verified against target contract.
|
||||
- **Developer Verification Strategy**:
|
||||
разработчики сами определяют unit/component/internal integration coverage
|
||||
и technical gates. Не копируй QA system verification matrix.
|
||||
- **UI/UX Package**:
|
||||
`new/changed`, `reuse` или `N/A`; relevant source/revision, screens, states,
|
||||
interactions, responsive/a11y expectations. Не требуй новый mockup для
|
||||
неизменяемого UI. Approved package remains accessible after archive.
|
||||
- **Risks / Trade-offs**.
|
||||
- **Migration / Rollout / Rollback**:
|
||||
implementation sequencing across repos, flags, data safety; mandatory
|
||||
rollback contract для security/destructive migration/public contract или
|
||||
другого труднообратимого решения, иначе `N/A` + reason.
|
||||
- **Solution Assessment**:
|
||||
обязателен для нового/существенно заменяемого auth, security, storage,
|
||||
external integration, observability, caching, concurrency, shared
|
||||
infrastructure/tooling или другого дорогостоящего необратимого решения.
|
||||
Сравни mature options по fit, maturity, security, operating cost,
|
||||
constraints и migration cost; custom implementation обоснуй конкретной
|
||||
несовместимостью alternatives. Не создавай отдельный artifact.
|
||||
- При UI/UX scope Design сохраняет package revision и exact immutable reuse
|
||||
locator (store ID и path от OpenSpec root), применимые части и отличия.
|
||||
Inline Markdown достаточен по умолчанию; необходимые PNG/SVG хранятся
|
||||
локально под `visuals/` с relative path, purpose и SHA-256 каждого файла.
|
||||
SVG самодостаточен: без scripts, handlers, external assets/fonts.
|
||||
External-only mutable URL или временный чат не заменяют согласованный
|
||||
пакет. Единственный approval Design охватывает всю версию пакета;
|
||||
self-hash Design не требуется. При Apply проверь наличие и SHA-256
|
||||
перечисленных материалов перед зависимой работой.
|
||||
|
||||
MUST NOT Include:
|
||||
- approved system-level sequence diagrams;
|
||||
- новые/изменённые API/event contracts без возврата в System Analysis;
|
||||
- новое behavior без возврата в Specs;
|
||||
- canonical docs update plan (это `docs-impact`);
|
||||
- QA system test cases/coverage strategy;
|
||||
- implementation checklist/PR plan/future results;
|
||||
- длинные production code listings.
|
||||
|
||||
Feedback Routing:
|
||||
- intent/scope → Proposal;
|
||||
- observable behavior → Specs;
|
||||
- systems/contracts/mappings/error semantics/semantic observability → System Analysis;
|
||||
- internal implementation choice → Design.
|
||||
|
||||
Validation and Approval:
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Reviewers: owners affected implementation repositories.
|
||||
- Остановись до explicit Design approval.
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: qa-analysis
|
||||
generates: qa-analysis.md
|
||||
description: Risk-based system verification analysis independent from developer-owned internal test architecture
|
||||
template: qa-analysis.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
QA Analysis отвечает на вопрос: "Что необходимо проверить, чтобы доказать
|
||||
работоспособность системы согласно Specs и System Analysis?"
|
||||
Это product/system verification artifact, не internal test design кода.
|
||||
|
||||
Preconditions:
|
||||
- Specs approved.
|
||||
- System Analysis approved.
|
||||
- QA Analysis может выполняться параллельно с Engineering Design.
|
||||
|
||||
Critical Rules:
|
||||
- QA проверяет system behavior, а не устройство кода.
|
||||
- Не предписывай разработчикам unit/component/internal integration tests,
|
||||
mocks внутри application code, coverage percentage, filenames/classes.
|
||||
- Не создавай новое behavior: undefined expected result → gap в Specs.
|
||||
- Undefined flow/error mapping/contract → gap в System Analysis.
|
||||
- Нужный testability hook/environment support формулируй как need для Design,
|
||||
но не проектируй его implementation за разработчика.
|
||||
- Manual QA и Automation QA используют один artifact. Automation scope —
|
||||
black-box API/cross-system/E2E. Internal code tests остаются developer-owned.
|
||||
- Не дублируй scenarios; ссылайся на exact Requirement/Scenario.
|
||||
|
||||
Required Shape:
|
||||
- **Verification Scope**: in/out capabilities/requirements/scenarios.
|
||||
- **Coverage Matrix**: Requirement/Scenario → system-level checks.
|
||||
- **Cross-system Verification**: contracts/error mapping/timeouts/unavailable/
|
||||
duplicates/out-of-order only where relevant.
|
||||
- **Risk-based Priorities**: critical journeys, money/security/data-integrity,
|
||||
compatibility/regression hotspots.
|
||||
- **Test Data and Environment Needs**: system-level states, accounts,
|
||||
permissions, external sandboxes/stubs; без internal mocking prescriptions.
|
||||
- **Manual / Exploratory Scope**.
|
||||
- **Automation Candidates**: black-box API/cross-system/E2E candidates.
|
||||
Пока отдельного QA repo нет, помечай repository automation как
|
||||
`deferred until QA repo exists`; не выдумывай paths/tasks.
|
||||
- **Observability Verification**: как на system level подтвердить semantic
|
||||
events (`NO_MONEY`, `FRAUD`, `BUTTON_CLICK` и т.п.), без SDK code.
|
||||
- **Regression Impact**.
|
||||
- **Gaps / Blockers**: каждый gap имеет routing destination.
|
||||
|
||||
MUST NOT Include:
|
||||
- unit/component/internal integration plan frontend/backend;
|
||||
- private code classes/functions/files;
|
||||
- application architecture decisions;
|
||||
- guessed expected behavior;
|
||||
- дублирование Design.
|
||||
|
||||
Review and Approval:
|
||||
- Owner: Manual QA / QA responsible for system verification.
|
||||
- Automation QA reviews automation feasibility when role exists.
|
||||
- Product/System Analyst reviews acceptance interpretation.
|
||||
- Affected engineers review testability dependencies.
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Остановись до explicit QA Analysis approval.
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: docs-impact
|
||||
generates: docs-impact.md
|
||||
description: Promotion plan from change-specific knowledge to canonical AS-IS documentation in docs repository
|
||||
template: docs-impact.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Docs Impact отвечает на вопрос: "Какое durable knowledge после успешной
|
||||
реализации должно изменить canonical AS-IS documentation в `docs/`?"
|
||||
Это planning artifact в `specs/`, а не сама документация.
|
||||
|
||||
Canonical Documentation Model:
|
||||
`docs/` — source of truth для актуального product/architecture description:
|
||||
- `README.md`, `overview.md`, `glossary.md` — navigation/product overview/terms;
|
||||
- `architecture/` — context, components, HTTP API, deployment;
|
||||
- `architecture/cross-cutting/` — cross-cutting system concerns;
|
||||
- `domains/` — durable domain/product behavior explanations;
|
||||
- `integrations/` — external systems and integrations;
|
||||
- `adr/` — historical architectural decisions; accepted ADR не переписывается
|
||||
под новое текущее состояние;
|
||||
- `design/` — durable visual/design principles.
|
||||
|
||||
Preconditions:
|
||||
- System Analysis approved.
|
||||
- Engineering Design approved.
|
||||
- Прочитай только релевантные current pages в `docs/` и не создавай duplicate
|
||||
page, если подходящее canonical location уже существует.
|
||||
|
||||
Critical Rules:
|
||||
- Не копируй весь change в docs. Promote только knowledge, полезное после
|
||||
того как change перестанет быть актуальной задачей.
|
||||
- Canonical docs описывают resulting AS-IS, а не migration history.
|
||||
- Migration rationale/history остаётся в archived OpenSpec change; если
|
||||
решение архитектурно значимо, создай/обнови ADR plan.
|
||||
- Не требуй переносить каждую system-analysis sequence. Promote только
|
||||
важные долгоживущие diagrams, которые помогают понимать текущую систему.
|
||||
- Не копируй machine-readable OpenAPI из System Analysis как второй canonical
|
||||
contract. `docs/` может описывать API/integration semantics и ссылаться на
|
||||
generated/runtime API source.
|
||||
- `docs-impact` не редактирует `docs/`; он создаёт точный promotion plan,
|
||||
из которого Tasks создаст `[docs]` implementation items.
|
||||
|
||||
Required Shape:
|
||||
- **Summary**: нужен ли durable docs update вообще.
|
||||
- **Documentation Changes**: таблица/список entries со свойствами:
|
||||
- `Action`: create | update | remove | none;
|
||||
- `Target`: exact path under `docs/`;
|
||||
- `Category`: overview | architecture | cross-cutting | domain |
|
||||
integration | ADR | design;
|
||||
- `Resulting AS-IS`: что именно должно стать истинным после change;
|
||||
- `Source`: ссылка на Specs/System Analysis/Design decision;
|
||||
- `Verification`: как понять, что canonical docs согласованы с реализацией.
|
||||
- **Diagram Promotion**: какие diagrams из `system-analysis/sequences/`
|
||||
становятся durable docs, куда именно и почему; default = не переносить.
|
||||
- **ADR Decision**:
|
||||
- `required` только если принято значимое, долгоживущее архитектурное
|
||||
решение с alternatives/consequences;
|
||||
- иначе `N/A` + reason.
|
||||
- **Glossary / Navigation Impact**: только если реально нужны новые термины
|
||||
или navigation entries.
|
||||
- **No-Change Rationale**: если docs не меняются, конкретно объясни почему.
|
||||
|
||||
MUST NOT Include:
|
||||
- временный migration diary как canonical AS-IS;
|
||||
- копию proposal/specs/system-analysis;
|
||||
- machine-readable API contract как второй source of truth;
|
||||
- implementation tasks; они создаются в Tasks;
|
||||
- fictional docs paths без проверки существующей структуры.
|
||||
|
||||
Review and Approval:
|
||||
- Owner: System Analyst + affected engineering owner совместно подтверждают,
|
||||
что planned docs отражают target system accurately.
|
||||
- Для ADR engineering owner подтверждает decision/consequences.
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Остановись до explicit Docs Impact approval.
|
||||
requires:
|
||||
- design
|
||||
- qa-analysis
|
||||
- system-analysis
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
description: Dependency-ordered, repository-owned delivery checklist derived from approved planning artifacts
|
||||
template: tasks.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Tasks — исполняемый delivery plan. Он отвечает только на вопрос:
|
||||
"Какие independently reviewable outcomes нужно доставить и проверить, чтобы
|
||||
реализовать approved plan?"
|
||||
|
||||
Preconditions:
|
||||
- Design, QA Analysis и Docs Impact explicitly approved.
|
||||
- Proposal/Specs/System Analysis остаются authoritative upstream contracts.
|
||||
|
||||
Repository Owners:
|
||||
- `[main]` — только workspace/dev/release orchestration changes.
|
||||
- `[frontend]` — React/Vite product implementation.
|
||||
- `[backend]` — NestJS/Prisma/SQLite product implementation.
|
||||
- `[docs]` — canonical AS-IS documentation changes из approved Docs Impact.
|
||||
- `[specs]` — только genuine specs-repository maintenance beyond native
|
||||
change/sync/archive (например schema/governance change).
|
||||
- `[qa]` — только после появления реального QA/E2E repository.
|
||||
|
||||
Task Format:
|
||||
Каждый checkbox:
|
||||
`- [ ] X.Y [main|frontend|backend|docs|specs|qa] <action + outcome>`
|
||||
и содержит:
|
||||
- **Covers:** exact `` `capability-path` — `Requirement heading` `` pairs
|
||||
либо `` `N/A` — <reason> ``.
|
||||
- **Source:** exact approved artifact section/contract/docs-impact entry.
|
||||
- **Verification:** owner-controlled command, observable check или delivered
|
||||
artifact; не будущий result.
|
||||
- **Depends on:** task IDs либо `none`, если зависимость неочевидна.
|
||||
|
||||
Critical Rules:
|
||||
- Одна task = один owner repository + один independently reviewable outcome.
|
||||
- Cross-repository work всегда разделяй.
|
||||
- Product feature code никогда не помещай в `[main]`.
|
||||
- `[docs]` tasks MUST соответствовать approved `docs-impact` entries; не
|
||||
придумывай документацию заново на этапе Tasks.
|
||||
- `[qa]` task запрещена, пока repo не существует. Manual QA/acceptance не
|
||||
маскируй под implementation checkbox.
|
||||
- Каждый Delta Requirement должен иметь implementation coverage либо ясное
|
||||
объяснение, почему implementation work не требуется.
|
||||
- При изменении contract обязательно запланируй verification, что
|
||||
backend-generated OpenAPI/runtime interface semantic-compatible с approved
|
||||
System Analysis contract до закрытия change.
|
||||
- Canonical behavior specs не обновляй вручную task'ом: их merge выполняет
|
||||
native OpenSpec sync/archive.
|
||||
- Developer-owned tests следуют Design; QA system verification следует
|
||||
QA Analysis. QA task не диктует unit/component tests разработчика.
|
||||
- Не создавай empty/ceremonial tasks.
|
||||
|
||||
Grouping:
|
||||
- Группируй numbered `##` sections по dependency/delivery sequence.
|
||||
- Repository ownership остаётся явным внутри каждой группы.
|
||||
- Documentation updates обычно выполняются после того, как target state
|
||||
подтверждён implementation, если только docs нужны раньше для coordination.
|
||||
|
||||
MUST NOT Include:
|
||||
- новое behavior → Specs;
|
||||
- новый/изменённый system contract/sequence/error semantics → System Analysis;
|
||||
- новый internal architecture decision → Design;
|
||||
- новый QA coverage decision → QA Analysis;
|
||||
- новый docs promotion decision → Docs Impact;
|
||||
- business rationale → Proposal;
|
||||
- duplicated payload/schema/design prose;
|
||||
- commits/branches/issues/post-hoc completion evidence.
|
||||
|
||||
Feedback Routing:
|
||||
- semantic change возвращается в самый ранний затронутый artifact и
|
||||
инвалидирует downstream approvals.
|
||||
- Vague/multi-owner/oversized decomposition исправляется здесь.
|
||||
|
||||
Validation and Approval:
|
||||
- После записи используй native OpenSpec validation against the resolved planning home.
|
||||
- Покажи Tasks Change Owner(s) и остановись до explicit approval.
|
||||
requires:
|
||||
- design
|
||||
- qa-analysis
|
||||
- docs-impact
|
||||
- specs
|
||||
|
||||
apply:
|
||||
requires:
|
||||
- tasks
|
||||
tracks: tasks.md
|
||||
instruction: |
|
||||
Purpose:
|
||||
Apply реализует approved delivery plan и не переоткрывает upstream decisions
|
||||
без явного feedback cycle.
|
||||
|
||||
Preconditions:
|
||||
- Explicit approvals: Proposal; Delta Specs если не `skip_specs`; System
|
||||
Analysis; Design; QA Analysis; Docs Impact; Tasks.
|
||||
- Используй canonical store `wealth-ledger-specs` для OpenSpec commands.
|
||||
|
||||
Context Loading:
|
||||
- Прочитай `contextFiles`, возвращённые native OpenSpec apply instructions.
|
||||
- Затем repository-local `AGENTS.md` только affected areas.
|
||||
- Для каждой task загружай релевантные upstream sections/contracts, а не весь
|
||||
workspace без необходимости.
|
||||
|
||||
Execution:
|
||||
- Выполняй pending tasks по dependency order.
|
||||
- Не объединяй разные repositories в одну task.
|
||||
- Product behavior реализуется только в `frontend/`/`backend/`; `main` меняется
|
||||
только при approved workspace/config impact.
|
||||
- Запускай task Verification и отмечай checkbox только после observed success.
|
||||
- Developer-owned unit/component/internal integration tests выполняются по
|
||||
Design/repository rules.
|
||||
- QA system/manual validation не считается выполненной только потому, что
|
||||
developer tests зелёные; она следует QA Analysis отдельным validation flow.
|
||||
- При changed contract backend-generated OpenAPI/runtime contract должен пройти
|
||||
semantic comparison с approved System Analysis target before completion.
|
||||
- `[docs]` tasks обновляют canonical `docs/` до resulting AS-IS из Docs Impact;
|
||||
не переносят change history вместо текущего состояния.
|
||||
- Если implementation-owned work остаётся, сохрани checkpoint: completed,
|
||||
remaining, observed checks и следующий concrete outcome.
|
||||
- READY_FOR_VALIDATION допустим после всех implementation/docs-owned tasks и
|
||||
relevant checks. Apply не подменяет independent QA/acceptance.
|
||||
|
||||
Sync / Archive Semantics:
|
||||
- Native OpenSpec sync/archive обновляет canonical behavior specs в `specs/`.
|
||||
- `docs/` обновляется только через approved `[docs]` tasks из Docs Impact.
|
||||
- Full System Analysis, target contracts и migration history остаются в
|
||||
archived change как historical planning evidence.
|
||||
- Significant architectural decision сохраняется отдельно как immutable ADR в
|
||||
`docs/adr/`; AS-IS architecture/integration pages показывают только current state.
|
||||
|
||||
Feedback Routing:
|
||||
- intent/scope/capabilities → Proposal;
|
||||
- observable behavior/invariants → Specs;
|
||||
- systems/sequences/contracts/mappings/error semantics/semantic observability
|
||||
→ System Analysis;
|
||||
- internal architecture/storage/UI implementation/technical observability/
|
||||
developer test strategy → Design;
|
||||
- system verification/test data/automation candidates → QA Analysis;
|
||||
- canonical docs promotion/ADR/diagram retention → Docs Impact;
|
||||
- decomposition/ownership/dependencies/task verification → Tasks.
|
||||
|
||||
Change Control:
|
||||
- При semantic conflict останови downstream work и сообщи evidence.
|
||||
- Инвалидируй approvals от самого раннего затронутого artifact.
|
||||
- Mechanical typo/formatting без normative meaning change может не требовать
|
||||
reapproval; Requirement/Scenario/contract/sequence/architecture/QA expected
|
||||
result/docs target/task verification semantic changes требуют reapproval.
|
||||
- Structural validation не заменяет semantic review, tests и independent QA.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Engineering Design
|
||||
|
||||
## Context and Constraints
|
||||
|
||||
## Technical Goals / Non-Goals
|
||||
|
||||
## Implementation Impact Map
|
||||
|
||||
### main
|
||||
|
||||
### frontend
|
||||
|
||||
### backend
|
||||
|
||||
### qa
|
||||
<!-- Use only after a real QA/E2E repository exists. -->
|
||||
|
||||
## Decisions
|
||||
|
||||
### main
|
||||
|
||||
### frontend
|
||||
|
||||
### backend
|
||||
|
||||
### qa
|
||||
|
||||
## Cross-cutting Implementation
|
||||
|
||||
## Data / Storage Changes
|
||||
|
||||
## Contract Implementation
|
||||
|
||||
## Developer Verification Strategy
|
||||
|
||||
## UI/UX Package
|
||||
|
||||
<!-- Choose new/changed, reuse or N/A with reason. For applicable UI include
|
||||
package revision, screens, states, interactions, responsive/a11y behavior.
|
||||
For reuse give immutable store ID + path from OpenSpec root, reused part
|
||||
and differences. Keep materials accessible after archive. Inline Markdown
|
||||
needs no self-hash; mutable external-only URLs are not an approved package. -->
|
||||
|
||||
**Applicability and scope:**
|
||||
|
||||
**Package revision:**
|
||||
|
||||
**Screens, states and interactions:**
|
||||
|
||||
**Responsive behavior:**
|
||||
|
||||
**Reused source and differences:**
|
||||
|
||||
<!-- If local PNG/SVG exports are needed, list each under visuals/ with a
|
||||
relative path, purpose and SHA-256. SVG must be self-contained (no scripts,
|
||||
handlers, external fonts/assets). Otherwise remove the table and state
|
||||
that the package is fully inline. One Design approval covers all materials. -->
|
||||
|
||||
| Material ID | Relative path | Screen/state and purpose | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| <!-- id --> | <!-- visuals/file.png --> | <!-- purpose --> | <!-- digest --> |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
## Migration / Rollout / Rollback
|
||||
|
||||
## Solution Assessment
|
||||
@@ -0,0 +1,16 @@
|
||||
# Documentation Impact
|
||||
|
||||
## Summary
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## Diagram Promotion
|
||||
|
||||
## ADR Decision
|
||||
|
||||
## Glossary / Navigation Impact
|
||||
|
||||
## No-Change Rationale
|
||||
@@ -0,0 +1,23 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
## What Changes
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
## Impact
|
||||
|
||||
## Success Criteria
|
||||
|
||||
## Assumptions / Dependencies
|
||||
@@ -0,0 +1,25 @@
|
||||
# QA Analysis
|
||||
|
||||
## Verification Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
### Out of Scope
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
## Cross-system Verification
|
||||
|
||||
## Risk-based Priorities
|
||||
|
||||
## Test Data and Environment Needs
|
||||
|
||||
## Manual / Exploratory Scope
|
||||
|
||||
## Automation Candidates
|
||||
|
||||
## Observability Verification
|
||||
|
||||
## Regression Impact
|
||||
|
||||
## Gaps / Blockers
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
Choose only the sections this delta needs; delete this guide before approval.
|
||||
|
||||
New capability only: add `## Purpose` with 1–2 sentences (50+ characters).
|
||||
Existing capability: do not add Purpose.
|
||||
|
||||
Supported operations:
|
||||
- `## ADDED Requirements`: new complete Requirement blocks.
|
||||
- `## MODIFIED Requirements`: full existing Requirement blocks with every
|
||||
surviving Scenario, then edited behavior; heading matches exactly.
|
||||
- `## REMOVED Requirements`: Requirement heading plus **Reason** and **Migration**.
|
||||
- `## RENAMED Requirements`: `FROM:` and `TO:` heading pairs only.
|
||||
|
||||
Every normative Requirement uses BCP 14 (RFC 2119 + RFC 8174) uppercase
|
||||
SHALL/MUST deliberately, and at least one `#### Scenario:` with `- **WHEN**`
|
||||
and `- **THEN**`. Lowercase must/should/may are ordinary language.
|
||||
Do not leave empty operation sections or a sample Requirement.
|
||||
-->
|
||||
@@ -0,0 +1,27 @@
|
||||
# System Analysis
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
## Systems and Responsibilities
|
||||
|
||||
## Interaction Flows
|
||||
|
||||
<!-- Store Mermaid sequence sources in system-analysis/sequences/*.mmd -->
|
||||
|
||||
## Contracts
|
||||
|
||||
<!-- Store OpenAPI/AsyncAPI/Proto/JSON Schema in system-analysis/contracts/ -->
|
||||
|
||||
## Data and State Semantics
|
||||
|
||||
## Error and Recovery Semantics
|
||||
|
||||
## Observability Semantics
|
||||
|
||||
## Security and NFR at System Boundaries
|
||||
|
||||
## Compatibility / Migration
|
||||
|
||||
## Durable Documentation Candidates
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,9 @@
|
||||
# Tasks
|
||||
|
||||
## 1. <delivery phase>
|
||||
|
||||
- [ ] 1.1 [backend] <action + independently reviewable outcome>
|
||||
- **Covers:** `N/A` — <reason or exact capability/requirement>
|
||||
- **Source:** <approved artifact section>
|
||||
- **Verification:** <command / observable check / delivered artifact>
|
||||
- **Depends on:** none
|
||||
@@ -15,7 +15,11 @@ Session-oriented SDD workflow SHALL направлять каждую agent-се
|
||||
phase record или ожидание решения MUST NOT само по себе принуждать к новой
|
||||
сессии той же роли. Specification MUST поддерживать bounded progression
|
||||
`Proposal + Delta Specs` и `Design + Tasks`, сохраняя отдельную explicit
|
||||
approval boundary перед каждым downstream artifact.
|
||||
approval boundary перед каждым downstream artifact. Для `wealth-ledger-sdd-v2`
|
||||
Specification SHALL также поддерживать System Analysis после Delta Specs,
|
||||
параллельные Design и QA Analysis после System Analysis, Docs Impact после
|
||||
обоих и Tasks после Docs Impact. Для active `wealth-ledger-sdd` сохраняются
|
||||
прежние пары и approvals без автоматической миграции.
|
||||
|
||||
#### Scenario: Начинается новый change
|
||||
|
||||
@@ -40,14 +44,14 @@ approval boundary перед каждым downstream artifact.
|
||||
|
||||
- **WHEN** Change Owner явно одобряет неизменившиеся Delta Specs
|
||||
- **THEN** workflow завершает bounded `Proposal + Delta Specs` progression и
|
||||
направляет следующий Specification outcome к Design
|
||||
направляет следующий Specification outcome к Design для legacy либо к System Analysis для v2
|
||||
|
||||
#### Scenario: Design явно одобрен
|
||||
|
||||
- **WHEN** Change Owner явно одобряет неизменившийся Design в bounded
|
||||
`Design + Tasks` progression
|
||||
- **THEN** Specification MAY продолжить в той же роли и создать Tasks без
|
||||
перехода к Implementation до отдельного approval Tasks
|
||||
- **THEN** для legacy Specification MAY продолжить в той же роли и создать Tasks;
|
||||
для v2 Tasks ожидает также QA Analysis и Docs Impact; Implementation ждёт отдельного approval Tasks
|
||||
|
||||
#### Scenario: Завершилась реализация
|
||||
|
||||
|
||||
@@ -127,3 +127,18 @@
|
||||
#### Scenario: Пагинация по применённому фильтру
|
||||
- **WHEN** User переходит к следующей или предыдущей странице журнала при изменённом, но не применённом черновике
|
||||
- **THEN** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется
|
||||
|
||||
### 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** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной
|
||||
|
||||
@@ -7,41 +7,53 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: User can retrieve upcoming events for an account
|
||||
Система MUST предоставлять аутентифицированному User read-only список известных будущих событий открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User, от момента запроса до 30 календарных дней после него включительно. Ответ MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза.
|
||||
Система 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 NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection.
|
||||
Ответ 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 запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection
|
||||
- **THEN** система возвращает известные события на следующие 30 календарных дней и момент, на который был выполнен расчёт
|
||||
- **WHEN** аутентифицированный User запрашивает события доступного счёта с последним полным прогнозом
|
||||
- **THEN** система возвращает из сохранённого прогноза события в запрошенном включительном UTC-окне, его исходные `calculatedAt` и рассчитанное окно без обращения к T-Bank
|
||||
|
||||
#### Scenario: Запрошенное окно не пересекается с сохранённым прогнозом
|
||||
- **WHEN** запрошенный включительный UTC-окно не пересекается с исходным окном последнего полного прогноза
|
||||
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`
|
||||
|
||||
#### Scenario: Отсутствие событий в периоде
|
||||
- **WHEN** для счёта нет известных событий в ближайшие 30 календарных дней
|
||||
- **THEN** система возвращает успешный пустой список и момент расчёта
|
||||
- **WHEN** пересечение запрошенного и исходного окна прогноза не содержит событий
|
||||
- **THEN** система возвращает успешный пустой список, момент расчёта, исходное окно и `meta`
|
||||
|
||||
#### Scenario: Некорректный период
|
||||
- **WHEN** User передаёт `from` позже `to`
|
||||
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD` без прогноза и без запуска личной синхронизации
|
||||
|
||||
#### Scenario: Прогноз всех счетов
|
||||
- **WHEN** User запрашивает режим `all` и для каждого входящего счёта доступен полный прогноз
|
||||
- **THEN** система возвращает события всех счетов в запрошенном окне с принадлежностью каждого события счёту
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
|
||||
- **WHEN** User запрашивает события счёта, недоступного через его текущий T-Bank connection
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
|
||||
|
||||
### Requirement: Forecasts preserve event meaning and uncertainty
|
||||
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по текущему количеству бумаг на момент запроса. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
|
||||
Система 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 Position имеет известный купон в пределах периода
|
||||
- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций
|
||||
- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза
|
||||
- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot
|
||||
|
||||
#### Scenario: Прогноз дивиденда
|
||||
- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода
|
||||
- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций
|
||||
- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза
|
||||
- **THEN** система возвращает сохранённое событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на количество акций этого Snapshot
|
||||
|
||||
#### Scenario: Отменённый дивиденд не попадает в прогноз
|
||||
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
|
||||
- **THEN** система не добавляет такое событие в результат
|
||||
- **THEN** система не добавляет такое событие в сохранённый прогноз
|
||||
|
||||
#### Scenario: Отображение оферты без суммы
|
||||
- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны
|
||||
- **WHEN** для поддерживаемой облигации известна оферта в пределах исходного окна прогноза, но её денежные условия недоступны
|
||||
- **THEN** система возвращает событие offer с прогнозной суммой `null`
|
||||
|
||||
#### Scenario: Неподдерживаемое частичное погашение
|
||||
@@ -49,42 +61,66 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
|
||||
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
|
||||
|
||||
### Requirement: Upcoming events use complete source results
|
||||
Система MUST завершать запрос ошибкой, если необходимые для прогноза данные брокера или MOEX недоступны, и MUST NOT возвращать частичный список как полный результат. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
|
||||
Система MUST публиковать прогноз только после полного получения необходимых данных брокера и MOEX для полного Portfolio Snapshot, по количеству бумаг которого он рассчитан. Неуспешное обновление MUST NOT заменять последний полный прогноз, его `calculatedAt` или окно событий частичным списком. При неуспешном обновлении и существующем последнем полном прогнозе система MUST возвращать отфильтрованную для запрошенного окна часть этого прогноза с его исходными `calculatedAt`, окном и `meta`, отдельно обозначающим неуспех последней попытки, даже если результат фильтрации пуст. При отсутствии последнего полного прогноза система MUST возвращать `data: null` и в `meta` обозначать выполнение первой загрузки либо её неуспех без частичного списка. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
|
||||
|
||||
#### Scenario: Недоступность источника
|
||||
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен
|
||||
- **THEN** система возвращает ошибку запроса будущих событий без частичного списка
|
||||
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен во время часового обновления
|
||||
- **THEN** система не публикует частичный список как полный прогноз
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе
|
||||
- **WHEN** часовой обход не завершает обновление необходимых источников, а последний полный прогноз существует
|
||||
- **THEN** система возвращает его события только в запрошенном окне, исходные `calculatedAt` и окно, а `meta` отдельно обозначает ошибку обновления
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе вне запрошенного окна
|
||||
- **WHEN** часовой обход не завершает обновление, последний полный прогноз существует, но его окно не пересекается с запрошенным
|
||||
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`, обозначающим ошибку обновления
|
||||
|
||||
#### Scenario: Инструмент без поддерживаемых данных
|
||||
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
|
||||
- **THEN** система не добавляет для него синтетическую строку в результат
|
||||
|
||||
#### Scenario: MOEX отвечает без запрошенного блока
|
||||
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
|
||||
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой
|
||||
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных
|
||||
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
|
||||
|
||||
### Requirement: Account page presents an upcoming events list
|
||||
Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать все полученные события компактным списком без фильтров, пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника.
|
||||
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
|
||||
|
||||
#### Scenario: Просмотр ближайших событий
|
||||
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
|
||||
- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы
|
||||
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы
|
||||
|
||||
#### Scenario: Пустое состояние страницы
|
||||
- **WHEN** запрос будущих событий успешно возвращает пустой список
|
||||
- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет ограничения покрытия источника
|
||||
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
|
||||
|
||||
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
|
||||
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
|
||||
|
||||
#### Scenario: Ошибка страницы
|
||||
- **WHEN** запрос будущих событий завершается ошибкой
|
||||
- **THEN** страница не показывает частичный или устаревший список и предоставляет действие повторить запрос
|
||||
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
|
||||
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
|
||||
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
|
||||
|
||||
### Requirement: Account overview includes upcoming events
|
||||
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio.
|
||||
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
|
||||
|
||||
#### Scenario: Отображение ближайших событий
|
||||
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
|
||||
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
|
||||
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
|
||||
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице
|
||||
|
||||
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
|
||||
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
|
||||
|
||||
#### Scenario: Ближайшие события недоступны
|
||||
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
|
||||
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
|
||||
|
||||
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
|
||||
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
|
||||
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
|
||||
|
||||
@@ -7,57 +7,65 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: Список активных брокерских счетов
|
||||
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
|
||||
Система 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`.
|
||||
Каждый элемент 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** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
|
||||
- **THEN** система отвечает `200` массивом брокерского счёта и ИИС в исходном порядке T-Bank без дебетового счёта
|
||||
- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
|
||||
- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank
|
||||
|
||||
#### Scenario: Пустой список поддерживаемых счетов
|
||||
- **WHEN** User с действующим T-Bank connection не имеет открытых поддерживаемых инвестиционных счетов
|
||||
- **THEN** система отвечает `200` пустым массивом
|
||||
- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов
|
||||
- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат
|
||||
|
||||
#### Scenario: Пустое название получает fallback
|
||||
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
|
||||
- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя
|
||||
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
|
||||
|
||||
#### Scenario: Дата открытия отсутствует
|
||||
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
|
||||
- **WHEN** последний подтверждённый счёт не имеет даты открытия
|
||||
- **THEN** система отвечает этим счётом с `openedOn: null`
|
||||
|
||||
#### Scenario: Мультивалютная сводка счёта
|
||||
- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
|
||||
- **WHEN** последний Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
|
||||
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
|
||||
|
||||
#### Scenario: Сводка одного счёта временно недоступна
|
||||
- **WHEN** список доступных счетов получен, но Portfolio одного из них временно недоступен
|
||||
- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка
|
||||
- **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio
|
||||
- **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null`
|
||||
|
||||
#### Scenario: Доступный Portfolio пуст
|
||||
- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков
|
||||
- **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков
|
||||
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||
|
||||
### Requirement: Состояния недоступности списка счетов
|
||||
Система MUST NOT возвращать частичный или сохранённый ранее список счетов. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклоняет сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При иной неуспешной попытке получить список из T-Bank система MUST отвечать `503` Problem Details с code `TBANK_UNAVAILABLE`.
|
||||
Система 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` и не удаляет connection
|
||||
- **WHEN** T-Bank отклоняет сохранённый token при часовом обходе списка как неаутентифицированный или без прав
|
||||
- **THEN** последующее чтение отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные
|
||||
|
||||
#### Scenario: Ошибка обхода после успешного списка
|
||||
- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки
|
||||
- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank
|
||||
|
||||
#### Scenario: T-Bank временно недоступен
|
||||
- **WHEN** запрос списка не завершается из-за временной либо иной upstream-ошибки
|
||||
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` без счетов
|
||||
- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой
|
||||
- **THEN** система сообщает о неуспешной первой загрузке без счетов
|
||||
|
||||
#### Scenario: Первый обход ещё не завершён
|
||||
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
|
||||
- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка
|
||||
|
||||
### Requirement: Экран списка брокерских счетов
|
||||
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать актуальный список; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
|
||||
Клиент 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 не выдавать отсутствие итогов за временную ошибку.
|
||||
|
||||
@@ -69,17 +77,29 @@
|
||||
- **WHEN** клиент получает успешный пустой массив
|
||||
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
|
||||
|
||||
#### Scenario: Последняя попытка неуспешна после полного списка
|
||||
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
|
||||
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке
|
||||
|
||||
#### Scenario: Первая загрузка списка выполняется или неуспешна
|
||||
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного
|
||||
|
||||
#### Scenario: Список берётся из свежего frontend-кэша
|
||||
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
|
||||
- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки
|
||||
- **THEN** клиент показывает сохранённые счета без нового API-запроса
|
||||
|
||||
#### Scenario: Устаревший список загружается повторно
|
||||
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
|
||||
- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление
|
||||
- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список
|
||||
- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление
|
||||
|
||||
#### Scenario: Открытый экран перечитывает список
|
||||
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
|
||||
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
|
||||
|
||||
#### Scenario: Ошибка фонового обновления не скрывает snapshot
|
||||
- **WHEN** обновление устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
|
||||
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки
|
||||
- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
|
||||
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки
|
||||
|
||||
#### Scenario: Карточка ограничивает валютные итоги
|
||||
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
|
||||
@@ -94,15 +114,15 @@
|
||||
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
|
||||
|
||||
### Requirement: Клиентские состояния ошибки списка счетов
|
||||
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия.
|
||||
Клиент 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** запрос списка завершается известной ошибкой
|
||||
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
|
||||
- **WHEN** HTTP-чтение списка завершается известной ошибкой
|
||||
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения
|
||||
|
||||
#### Scenario: Token требует внимания
|
||||
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
|
||||
@@ -110,12 +130,12 @@
|
||||
|
||||
#### Scenario: Временная ошибка T-Bank
|
||||
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
|
||||
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||
- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
|
||||
|
||||
#### Scenario: Неизвестная ошибка загрузки
|
||||
- **WHEN** клиент получает ошибку без известного кода списка счетов
|
||||
- **THEN** клиент не показывает список и предлагает только повторить загрузку
|
||||
- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение
|
||||
|
||||
#### Scenario: Известная ошибка при сохранённом snapshot
|
||||
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
|
||||
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||
- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
|
||||
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
|
||||
|
||||
@@ -9,28 +9,20 @@
|
||||
## 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`.
|
||||
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 и начинается
|
||||
фоновое обновление того же списка
|
||||
- **THEN** frontend оставляет список видимым и показывает только
|
||||
неблокирующий индикатор обновления
|
||||
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое HTTP-перечитывание того же списка
|
||||
- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор перечитывания
|
||||
|
||||
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
|
||||
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot
|
||||
Portfolio, а последующее обновление этого же Portfolio завершается ошибкой
|
||||
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное
|
||||
неблокирующее предупреждение о показе последних полученных данных и
|
||||
предоставляет повторную попытку обновления
|
||||
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее HTTP-чтение завершается ошибкой
|
||||
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение и предоставляет повтор только HTTP-чтения
|
||||
|
||||
#### Scenario: Server-side fallback и client cached snapshot различаются
|
||||
- **WHEN** backend возвращает Portfolio с `isStale` либо фоновое обновление
|
||||
Client Cached Snapshot завершается ошибкой
|
||||
- **THEN** frontend отображает соответствующее server-side либо client-side
|
||||
предупреждение независимо, не представляя один вид состояния другим
|
||||
- **WHEN** backend возвращает Portfolio с `isStale` либо HTTP-перечитывание Client Cached Snapshot завершается ошибкой
|
||||
- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя одно состояние другим
|
||||
|
||||
#### Scenario: Resource сохраняется после неуспешной часовой попытки
|
||||
- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки
|
||||
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# hourly-investment-sync Specification
|
||||
|
||||
## Purpose
|
||||
Определяет независимое от открытых экранов часовое обновление личных инвестиционных данных и безопасно наблюдаемый результат последней попытки.
|
||||
|
||||
## 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** система не раскрывает результат прежней границы подключения
|
||||
@@ -0,0 +1,680 @@
|
||||
# manual-bank-cards Specification
|
||||
|
||||
## Purpose
|
||||
Позволяет пользователю вручную вести дебетовые банковские карты в рублях:
|
||||
сохранять операции, видеть независимые балансы, исправлять записи и оценивать
|
||||
месячные расходы по категориям с учётом возвратов.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Отдельный раздел ручного учёта банковских карт
|
||||
|
||||
Система SHALL предоставлять авторизованному пользователю раздел банковских
|
||||
карт на `/cards` и просмотр отдельной своей карты на `/cards/:id`.
|
||||
Раздел MUST работать без подключения T-Bank и не зависеть от доступности
|
||||
инвестиционных данных. Банковские карты MUST NOT становиться брокерскими
|
||||
счетами, а банковские суммы и операции MUST NOT включаться в инвестиционный
|
||||
портфель, пассивный доход или прогноз выплат.
|
||||
|
||||
Первая версия SHALL поддерживать только ручной учёт дебетовых карт в RUB.
|
||||
Автоматическая загрузка из банков, кредитные карты, другие валюты, общий
|
||||
денежный баланс нескольких карт, прогнозы и бюджеты не входят в возможность.
|
||||
|
||||
#### Scenario: Работа без инвестиционного подключения
|
||||
|
||||
- **WHEN** пользователь с действующей локальной сессией, но без подключения T-Bank, открывает раздел карт
|
||||
- **THEN** он может просматривать и вести свои банковские карты без перехода к подключению брокера
|
||||
|
||||
#### Scenario: Независимость от инвестиционных данных
|
||||
|
||||
- **WHEN** пользователь сохраняет банковское пополнение или расход, а инвестиционные данные временно недоступны
|
||||
- **THEN** банковская операция сохраняется по правилам ручного учёта и не меняет инвестиционные суммы или пассивный доход
|
||||
|
||||
### Requirement: Ручное добавление независимых банковских карт
|
||||
|
||||
Система SHALL позволять пользователю добавить карту с названием, начальным
|
||||
остатком и датой начала учёта. Название MUST содержать от 1 до 100 символов
|
||||
после удаления внешних пробелов. Название банка необязательно и MUST содержать
|
||||
не более 100 символов после удаления внешних пробелов. Последние четыре цифры
|
||||
карты необязательны; указанное значение MUST состоять ровно из четырёх цифр.
|
||||
Полный номер карты, срок действия, PIN и код безопасности не запрашиваются
|
||||
и не сохраняются как реквизиты этой возможности.
|
||||
|
||||
Каждая карта MUST иметь собственную идентичность, баланс и историю независимо
|
||||
от совпадения банка, названия или последних цифр. Пользователь SHALL иметь
|
||||
возможность изменить указанные сведения о своей карте с теми же ограничениями.
|
||||
Финансовые изменения начального остатка и даты начала подчиняются отдельным
|
||||
требованиям исправления и архива.
|
||||
|
||||
#### Scenario: Добавление карты с начальным остатком
|
||||
|
||||
- **WHEN** пользователь сохраняет карту с допустимым названием, датой начала учёта и начальным остатком 10 000 ₽
|
||||
- **THEN** карта появляется в его списке с балансом 10 000 ₽ и без вручную внесённых операций
|
||||
|
||||
#### Scenario: Необязательные сведения не заполнены
|
||||
|
||||
- **WHEN** пользователь добавляет карту без названия банка и последних четырёх цифр
|
||||
- **THEN** отсутствие этих сведений не препятствует сохранению карты при корректности обязательных значений
|
||||
|
||||
#### Scenario: Несколько карт одного банка
|
||||
|
||||
- **WHEN** пользователь добавляет две карты одного банка с одинаковым названием и начальными остатками 1 000 ₽ и 2 000 ₽
|
||||
- **THEN** система сохраняет две самостоятельные карты с раздельными балансами и историей
|
||||
|
||||
#### Scenario: Некорректные сведения карты
|
||||
|
||||
- **WHEN** название после удаления внешних пробелов пустое или длиннее 100 символов, название банка длиннее 100 символов либо указанные последние цифры не состоят ровно из четырёх цифр
|
||||
- **THEN** система отклоняет сохранение, объясняет ошибку соответствующего поля и не создаёт или не изменяет карту
|
||||
|
||||
### Requirement: Точные денежные значения и проверка до сохранения
|
||||
|
||||
Система MUST принимать денежные значения только в RUB, с точностью не более
|
||||
двух знаков после десятичного разделителя, без округления введённой суммы.
|
||||
Сумма расхода, пополнения, перевода или возврата MUST быть строго положительной
|
||||
и не превышать 92 233 720 368 547 758,07 ₽. Начальный остаток MUST находиться
|
||||
в диапазоне от −92 233 720 368 547 758,08 ₽ до 92 233 720 368 547 758,07 ₽
|
||||
включительно; ноль и отрицательный начальный остаток допустимы.
|
||||
|
||||
Значение, не представляющее корректную денежную сумму, неподдерживаемая
|
||||
валюта, лишние десятичные знаки или выход за диапазон MUST приводить
|
||||
к понятной ошибке без частичного сохранения. Применение ограничения
|
||||
к каждому денежному вводу MUST NOT приводить к округлению, переполнению
|
||||
или ограничению точного результата сложения уже сохранённых сумм.
|
||||
|
||||
#### Scenario: Нулевой и отрицательный начальный остаток
|
||||
|
||||
- **WHEN** пользователь сохраняет отдельные карты с начальными остатками 0 ₽ и −100 ₽
|
||||
- **THEN** система сохраняет обе карты и показывает соответствующие известные балансы
|
||||
|
||||
#### Scenario: Неположительная сумма операции
|
||||
|
||||
- **WHEN** пользователь пытается внести операцию с суммой 0 ₽ или −1 ₽
|
||||
- **THEN** система отклоняет операцию, сохраняя прежние историю, балансы и сводки
|
||||
|
||||
#### Scenario: Точность не исправляется округлением
|
||||
|
||||
- **WHEN** пользователь пытается сохранить начальный остаток или операцию со значением 1,001 ₽
|
||||
- **THEN** система сообщает о допустимых двух десятичных знаках и не заменяет введённое значение на 1 ₽
|
||||
|
||||
#### Scenario: Границы денежных значений
|
||||
|
||||
- **WHEN** пользователь указывает верхнюю допустимую сумму операции или нижний допустимый начальный остаток
|
||||
- **THEN** сами эти граничные значения принимаются, а выход за соответствующую границу на одну копейку отклоняется без изменения данных
|
||||
|
||||
#### Scenario: Некорректная сумма или валюта
|
||||
|
||||
- **WHEN** поступает попытка сохранить нечисловую сумму либо сумму в валюте, отличной от RUB
|
||||
- **THEN** система объясняет ошибку и не сохраняет карту, операцию или изменение существующих данных
|
||||
|
||||
### Requirement: Даты начала учёта и ручных операций
|
||||
|
||||
Дата начала учёта и дата операции MUST быть существующими календарными датами
|
||||
без времени. Сегодня определяется по Europe/Moscow. Дата начала учёта
|
||||
MUST NOT быть позже сегодняшней даты. Начальный остаток означает деньги
|
||||
до всех вручную внесённых операций начиная с указанной даты включительно.
|
||||
|
||||
Дата операции MUST находиться между датой начала учёта затронутой карты
|
||||
и сегодняшним днём включительно. Для перевода ограничение MUST выполняться
|
||||
для обеих карт. Ввод прошедших операций внутри этого интервала SHALL быть
|
||||
доступен. Необязательное описание операции MUST содержать не более
|
||||
500 символов. Сохранённая календарная дата MUST NOT сдвигаться при смене
|
||||
часового пояса устройства.
|
||||
|
||||
#### Scenario: Операция в первый день учёта
|
||||
|
||||
- **WHEN** при начальном остатке 1 000 ₽ на 1 сентября пользователь вносит расход 100 ₽ с датой 1 сентября
|
||||
- **THEN** расход учитывается после начального остатка и баланс становится 900 ₽
|
||||
|
||||
#### Scenario: Прошедшая операция
|
||||
|
||||
- **WHEN** пользователь вносит операцию задним числом не раньше начала учёта и не позже сегодняшнего дня
|
||||
- **THEN** операция сохраняется с указанной датой и изменяет текущий баланс и сводку соответствующего месяца
|
||||
|
||||
#### Scenario: Недопустимая дата или описание
|
||||
|
||||
- **WHEN** дата не существует, находится до начала учёта или в будущем либо описание длиннее 500 символов
|
||||
- **THEN** система объясняет ошибку и не сохраняет изменение
|
||||
|
||||
#### Scenario: Перевод до начала учёта получателя
|
||||
|
||||
- **WHEN** дата перевода допустима для карты-источника, но предшествует началу учёта карты-получателя
|
||||
- **THEN** перевод отклоняется без изменения обеих карт
|
||||
|
||||
#### Scenario: Московская календарная граница
|
||||
|
||||
- **WHEN** на устройстве ещё предыдущий день, а в Europe/Moscow уже наступил следующий
|
||||
- **THEN** допустимая сегодняшняя дата и текущий месяц определяются по Europe/Moscow, а сохранённые даты операций остаются прежними
|
||||
|
||||
### Requirement: Баланс определяется начальным остатком и внесёнными операциями
|
||||
|
||||
Баланс карты MUST равняться начальному остатку плюс денежное влияние всех
|
||||
её сохранённых и не удалённых операций: пополнение и возврат увеличивают
|
||||
баланс, расход уменьшает, перевод уменьшает баланс источника и увеличивает
|
||||
баланс получателя. Начальный остаток MUST учитываться ровно один раз
|
||||
и MUST NOT считаться расходом, возвратом или пополнением.
|
||||
|
||||
Баланс MUST рассчитываться точно и может быть отрицательным. Недостаточный
|
||||
учётный остаток MUST NOT препятствовать сохранению корректного расхода
|
||||
или перевода. Баланс MUST NOT зависеть от выбранного месяца, категории,
|
||||
показанной страницы истории или нахождения карты в архиве.
|
||||
|
||||
Система SHALL представлять баланс как результат ручного учёта и MUST NOT
|
||||
выдавать его за подтверждённый банком остаток или обещать полноту невнесённых
|
||||
операций. Отдельной произвольной операции корректировки баланса SHALL NOT быть.
|
||||
|
||||
#### Scenario: Последовательные расходы с копейками
|
||||
|
||||
- **WHEN** при начальном остатке 1 ₽ сохраняются расходы 0,10 ₽ и 0,20 ₽
|
||||
- **THEN** баланс составляет ровно 0,70 ₽
|
||||
|
||||
#### Scenario: Полное расходование и отрицательный остаток
|
||||
|
||||
- **WHEN** при балансе 100 ₽ пользователь сохраняет расход 100 ₽, затем расход 50 ₽
|
||||
- **THEN** первая операция даёт известный баланс 0 ₽, вторая сохраняется и даёт баланс −50 ₽
|
||||
|
||||
#### Scenario: Начальный остаток и страница истории
|
||||
|
||||
- **WHEN** при начальном остатке 1 000 ₽ и расходах 100 ₽ и 200 ₽ пользователь повторно открывает карту и видит только расход 200 ₽
|
||||
- **THEN** текущий баланс остаётся 700 ₽: учитываются обе операции, а начальный остаток не добавляется повторно
|
||||
|
||||
#### Scenario: Итог превышает границу отдельного ввода
|
||||
|
||||
- **WHEN** на карту с начальным остатком 92 233 720 368 547 758,07 ₽ сохраняется пополнение 0,01 ₽
|
||||
- **THEN** баланс составляет точно 92 233 720 368 547 758,08 ₽ без переполнения, округления или ограничения верхней границей ввода
|
||||
|
||||
### Requirement: Ручное внесение расходов и пополнений
|
||||
|
||||
Система SHALL позволять вручную внести расход или пополнение по своей активной
|
||||
карте с суммой, датой и необязательным описанием. Расход MUST иметь категорию;
|
||||
если пользователь не выбирает другую, применяется его категория по умолчанию.
|
||||
После успешного сохранения операция MUST появиться в истории соответствующей
|
||||
карты и ровно один раз изменить её баланс.
|
||||
|
||||
Расход SHALL уменьшать баланс на свою сумму, пополнение SHALL увеличивать.
|
||||
Категория MUST NOT изменять денежное влияние расхода. Пополнение MUST NOT
|
||||
включаться в расходы, возвраты или пассивный инвестиционный доход.
|
||||
|
||||
#### Scenario: Расход с категорией
|
||||
|
||||
- **WHEN** при балансе 10 000 ₽ пользователь сохраняет расход 500 ₽ с категорией «Кафе»
|
||||
- **THEN** история показывает расход 500 ₽ с этой категорией, а баланс составляет 9 500 ₽
|
||||
|
||||
#### Scenario: Категория по умолчанию
|
||||
|
||||
- **WHEN** пользователь сохраняет расход, не выбрав другую категорию
|
||||
- **THEN** расход сохраняется с принадлежащей пользователю категорией по умолчанию и отображается под её текущим названием
|
||||
|
||||
#### Scenario: Пополнение карты
|
||||
|
||||
- **WHEN** при балансе −500 ₽ пользователь сохраняет пополнение 2 000 ₽
|
||||
- **THEN** баланс становится 1 500 ₽, история содержит пополнение, а расходы и возвраты не меняются
|
||||
|
||||
#### Scenario: Независимость другой карты
|
||||
|
||||
- **WHEN** при балансах A 1 000 ₽ и B 2 000 ₽ пользователь сохраняет расход 125,50 ₽ по A
|
||||
- **THEN** баланс A становится 874,50 ₽, а баланс и история B остаются прежними
|
||||
|
||||
### Requirement: Перевод между своими картами как одно действие
|
||||
|
||||
Система SHALL позволять одним действием внести перевод между двумя разными
|
||||
активными картами одного пользователя. Сумма перевода MUST одинаково уменьшать
|
||||
баланс источника и увеличивать баланс получателя. История каждой стороны
|
||||
MUST показывать направление и другую карту этого перевода.
|
||||
|
||||
Перевод MUST сохраняться, исправляться и удаляться целиком для обеих сторон.
|
||||
Нельзя сохранять только одну сторону, переводить на ту же или чужую карту.
|
||||
Перевод MUST NOT увеличивать расходы или возвраты в сводках одной или всех карт.
|
||||
|
||||
#### Scenario: Перевод между картами
|
||||
|
||||
- **WHEN** при балансах A 5 000 ₽ и B 1 000 ₽ пользователь сохраняет перевод 2 000 ₽ из A в B
|
||||
- **THEN** баланс A становится 3 000 ₽, баланс B — 3 000 ₽, обе истории показывают стороны одного перевода, а расходы не меняются
|
||||
|
||||
#### Scenario: Недостаточный остаток источника
|
||||
|
||||
- **WHEN** при балансах A 100 ₽ и B 0 ₽ пользователь сохраняет перевод 150 ₽ из A в B
|
||||
- **THEN** перевод сохраняется целиком, баланс A становится −50 ₽, а баланс B — 150 ₽
|
||||
|
||||
#### Scenario: Недопустимый получатель
|
||||
|
||||
- **WHEN** получателем указана та же карта или карта другого пользователя
|
||||
- **THEN** система отклоняет перевод без изменения историй, балансов и сводок
|
||||
|
||||
#### Scenario: Сбой сохранения перевода
|
||||
|
||||
- **WHEN** перевод не удаётся сохранить
|
||||
- **THEN** система не подтверждает успех и не оставляет сохранённой только одну его сторону
|
||||
|
||||
### Requirement: Возврат связан с исходным расходом
|
||||
|
||||
Возврат SHALL ссылаться на существующий не удалённый расход той же карты.
|
||||
При внесении возврата пользователь SHALL иметь возможность выбрать исходный
|
||||
расход карты за любой период учёта, независимо от месяца открытого журнала.
|
||||
Частичные и несколько возвратов допустимы; сумма всех не удалённых возвратов
|
||||
MUST NOT превышать текущую сумму исходного расхода. Дата возврата MUST быть
|
||||
не раньше даты расхода и соответствовать общим ограничениям даты карты.
|
||||
При редактировании возврата проверка общей возвращённой суммы MUST заменять
|
||||
его прежнюю сумму новой, не учитывая обе версии одновременно.
|
||||
|
||||
Возврат MUST увеличивать баланс карты и наследовать категорию исходного
|
||||
расхода. Отдельный выбор категории возврата SHALL NOT предоставляться.
|
||||
Изменение категории исходного расхода MUST менять категорию связанных
|
||||
возвратов и результаты затронутых месячных сводок, сохраняя их суммы и даты.
|
||||
|
||||
#### Scenario: Частичные возвраты
|
||||
|
||||
- **WHEN** по расходу 1 000 ₽ пользователь последовательно сохраняет возвраты 300 ₽ и 700 ₽
|
||||
- **THEN** оба возврата связаны с расходом, увеличивают баланс суммарно на 1 000 ₽ и используют его категорию
|
||||
|
||||
#### Scenario: Выбор покупки прошлого месяца
|
||||
|
||||
- **WHEN** пользователь открывает сентябрьский журнал и вносит возврат по августовской покупке
|
||||
- **THEN** форма позволяет найти и выбрать исходный августовский расход этой карты без ограничения сентябрьским журналом
|
||||
|
||||
#### Scenario: Исправление полностью возвращённой покупки
|
||||
|
||||
- **WHEN** по расходу 500 ₽ сохранён единственный возврат 500 ₽ и пользователь изменяет только описание возврата
|
||||
- **THEN** исправление сохраняется: общая возвращённая сумма остаётся 500 ₽, а прежняя и новая версии не складываются как два возврата
|
||||
|
||||
#### Scenario: Превышение возвращаемой суммы
|
||||
|
||||
- **WHEN** при расходе 1 000 ₽ и сохранённом возврате 800 ₽ пользователь пытается сохранить ещё 300 ₽
|
||||
- **THEN** система отклоняет новый возврат без изменения существующих записей и сумм
|
||||
|
||||
#### Scenario: Недопустимая связь или дата возврата
|
||||
|
||||
- **WHEN** возврат ссылается на удалённую операцию, операцию другого вида или другой карты либо датирован раньше расхода
|
||||
- **THEN** система отклоняет возврат без изменения истории, баланса и сводок
|
||||
|
||||
#### Scenario: Изменение категории покупки
|
||||
|
||||
- **WHEN** пользователь меняет категорию расхода с «Покупки» на «Здоровье», а возврат по нему произошёл в другом месяце
|
||||
- **THEN** расход и возврат учитываются в категории «Здоровье» в своих месяцах с прежними денежными суммами
|
||||
|
||||
### Requirement: Общие для пользователя категории расходов
|
||||
|
||||
Система SHALL предоставлять каждому пользователю собственный общий для всех
|
||||
его карт список категорий: «Продукты», «Кафе», «Такси», «Транспорт», «Жильё»,
|
||||
«Здоровье», «Покупки», «Развлечения», «Другое». «Другое» является начальной
|
||||
категорией по умолчанию. Категории MUST быть доступны до первого расхода.
|
||||
Начальный набор MUST создаваться при успешном добавлении первой карты
|
||||
пользователя и не дублироваться при добавлении следующих карт.
|
||||
|
||||
Пользователь SHALL иметь возможность добавлять и переименовывать любые свои
|
||||
категории, включая начальные. Название MUST содержать от 1 до 80 символов
|
||||
после удаления внешних пробелов и быть уникальным среди категорий пользователя
|
||||
без учёта регистра. Переименование MUST сохранять идентичность категории:
|
||||
все старые расходы, возвраты и сводки показывают её текущее название.
|
||||
Переименование категории по умолчанию MUST сохранять её роль.
|
||||
Удаление категорий и вложенные подкатегории SHALL NOT предоставляться в V1.
|
||||
|
||||
#### Scenario: Начальный набор и повторный вход
|
||||
|
||||
- **WHEN** пользователь сохраняет первую карту, затем добавляет следующую карту и повторно входит в приложение
|
||||
- **THEN** после первой карты ему доступен начальный набор категорий до первого расхода, а дальнейшие действия не создают дубликатов
|
||||
|
||||
#### Scenario: Категория доступна на всех своих картах
|
||||
|
||||
- **WHEN** пользователь создаёт категорию «Питомец»
|
||||
- **THEN** она доступна для расходов по всем его картам и не появляется у другого пользователя
|
||||
|
||||
#### Scenario: Переименование используемой начальной категории
|
||||
|
||||
- **WHEN** пользователь переименовывает категорию «Другое» в «Остальное»
|
||||
- **THEN** прежние записи отображаются как «Остальное», суммы не меняются, а новые расходы без другого выбора используют ту же категорию «Остальное»
|
||||
|
||||
#### Scenario: Дублирующееся или недопустимое название
|
||||
|
||||
- **WHEN** пользователь пытается добавить « кафе » при существующей «Кафе», сохранить пустое после удаления пробелов название или название длиннее 80 символов
|
||||
- **THEN** система объясняет ошибку и не изменяет список категорий
|
||||
|
||||
### Requirement: Исправление и удаление операций с согласованным пересчётом
|
||||
|
||||
Пользователь SHALL иметь возможность изменить сумму, дату, описание
|
||||
и применимые реквизиты сохранённой операции либо удалить её. Вид операции
|
||||
и связь возврата с исходным расходом MUST быть неизменны после создания.
|
||||
Все затронутые карты MUST быть активны; каждое исправление MUST соблюдать
|
||||
те же денежные, календарные и пользовательские ограничения, что и создание.
|
||||
|
||||
Обычный расход или пополнение SHALL быть переносимы на другую свою активную
|
||||
карту. Расход с не удалёнными возвратами MUST NOT переноситься на другую карту,
|
||||
удаляться, уменьшаться ниже суммы возвратов или сдвигаться на дату позже
|
||||
любого связанного возврата. Для такого действия сначала исправляются
|
||||
или удаляются мешающие возвраты. Возврат остаётся на карте исходного расхода.
|
||||
|
||||
Исправление или удаление MUST пересчитать балансы, историю и все затронутые
|
||||
месячные сводки; изменение даты переносит влияние между соответствующими
|
||||
месяцами. Исправление сторон перевода MUST учитывать его прежние и новые
|
||||
карты. Удаление перевода MUST удалять обе стороны одним действием.
|
||||
Повторное удаление уже удалённой собственной операции SHALL быть
|
||||
безрезультатным успешным действием без повторного денежного влияния.
|
||||
|
||||
#### Scenario: Изменение суммы и месяца расхода
|
||||
|
||||
- **WHEN** пользователь меняет расход 100 ₽ за август на 150 ₽ за сентябрь
|
||||
- **THEN** баланс дополнительно уменьшается на 50 ₽, август перестаёт учитывать 100 ₽, а сентябрь учитывает 150 ₽
|
||||
|
||||
#### Scenario: Перенос пополнения на другую карту
|
||||
|
||||
- **WHEN** пользователь переносит пополнение 500 ₽ с A на свою активную B с допустимой датой
|
||||
- **THEN** баланс A уменьшается на 500 ₽, баланс B увеличивается на 500 ₽, а запись относится к B
|
||||
|
||||
#### Scenario: Изменение получателя перевода
|
||||
|
||||
- **WHEN** перевод 200 ₽ из A в B исправляется на перевод 300 ₽ из A в C и все три карты активны
|
||||
- **THEN** прежнее влияние на B отменяется, A уменьшается дополнительно на 100 ₽, C увеличивается на 300 ₽, а обе стороны исправленного перевода сохраняются согласованно
|
||||
|
||||
#### Scenario: Исправление расхода ограничено возвратами
|
||||
|
||||
- **WHEN** для расхода 1 000 ₽ существует возврат 600 ₽ и пользователь пытается удалить расход, перенести его на другую карту, уменьшить до 500 ₽ или сдвинуть позже даты возврата
|
||||
- **THEN** система отклоняет действие и объясняет необходимость сначала исправить связанные возвраты
|
||||
|
||||
#### Scenario: Удаление возврата и затем расхода
|
||||
|
||||
- **WHEN** пользователь удаляет все возвраты расхода, а затем сам расход
|
||||
- **THEN** влияние каждой удалённой записи отменяется в балансе и в её месячной сводке, а ограничение удаления исходного расхода больше не действует
|
||||
|
||||
#### Scenario: Удаление перевода из любой стороны
|
||||
|
||||
- **WHEN** пользователь удаляет перевод из истории получателя и затем повторяет удаление
|
||||
- **THEN** обе стороны удаляются при первом действии, денежное влияние отменяется один раз, а повторное удаление ничего не меняет
|
||||
|
||||
#### Scenario: Смена вида или исходного расхода
|
||||
|
||||
- **WHEN** пользователь пытается превратить расход в пополнение либо связать существующий возврат с другим расходом
|
||||
- **THEN** изменение отклоняется без подмены вида операции или её исходной связи
|
||||
|
||||
### Requirement: Исправление начала ручного учёта
|
||||
|
||||
Пользователь SHALL иметь возможность изменить начальный остаток и дату начала
|
||||
учёта активной карты. Изменение MUST проверять всю затронутую сохранённую
|
||||
историю, включая стороны переводов и связанные возвраты, и отклоняться
|
||||
без частичного результата, если после него какая-либо операция нарушает
|
||||
действующие ограничения. Удалённые операции не ограничивают новую дату.
|
||||
|
||||
Изменение начального остатка SHALL пересчитывать текущий баланс, сохраняя
|
||||
суммы, даты и категории операций. Оно MUST NOT создавать отдельную операцию
|
||||
или изменять месячные расходы и возвраты. Пропущенное движение SHALL
|
||||
исправляться внесением реальной операции с допустимой датой.
|
||||
|
||||
#### Scenario: Исправление начального остатка
|
||||
|
||||
- **WHEN** при начальном остатке 1 000 ₽ и расходах 300 ₽ пользователь меняет начальный остаток на 1 200 ₽
|
||||
- **THEN** баланс становится 900 ₽, история операций не пополняется корректировкой, а месячные расходы остаются 300 ₽
|
||||
|
||||
#### Scenario: Новое начало противоречит истории
|
||||
|
||||
- **WHEN** пользователь переносит дату начала учёта с 1 на 10 сентября, а по карте сохранён расход или перевод от 5 сентября
|
||||
- **THEN** система отклоняет изменение и сохраняет прежние дату, баланс и всю историю
|
||||
|
||||
### Requirement: Архив карт сохраняет историю
|
||||
|
||||
Пользователь SHALL иметь возможность архивировать свою карту при любом
|
||||
балансе, включая положительный, нулевой и отрицательный, и вернуть её
|
||||
из архива. Архивация MUST NOT менять деньги или операции. Обычный список
|
||||
SHALL показывать активные карты; переключатель архива SHALL открывать
|
||||
архивные карты и их историю.
|
||||
|
||||
Пока карта архивна, система MUST запрещать создание, редактирование
|
||||
и удаление затрагивающих её операций, а также изменение начального остатка
|
||||
и даты начала учёта. Для исправления или новой операции карту сначала
|
||||
возвращают из архива. Перевод или возврат MUST требовать активного состояния
|
||||
всех затронутых карт. Изменение описательных сведений карты само по себе
|
||||
не является финансовой операцией.
|
||||
|
||||
Архивная карта MUST продолжать участвовать в исторических сводках «Все карты».
|
||||
Безвозвратное удаление карты SHALL NOT предоставляться в первой версии.
|
||||
|
||||
#### Scenario: Архивирование с ненулевым остатком
|
||||
|
||||
- **WHEN** пользователь архивирует карту с балансом −500 ₽ и сохранённой историей
|
||||
- **THEN** карта скрывается из обычного списка, остаётся доступной через переключатель архива с теми же балансом и историей и сохраняет вклад в сводки всех карт
|
||||
|
||||
#### Scenario: Финансовое действие по архивной карте
|
||||
|
||||
- **WHEN** пользователь пытается внести, исправить или удалить операцию либо изменить начало учёта архивной карты
|
||||
- **THEN** система отклоняет действие без изменения данных и объясняет необходимость вернуть карту из архива
|
||||
|
||||
#### Scenario: Возврат или перевод затрагивает архив
|
||||
|
||||
- **WHEN** возврат относится к расходу архивной карты либо хотя бы одна прежняя или новая сторона изменяемого перевода архивна
|
||||
- **THEN** финансовое действие отклоняется до возвращения всех затронутых карт из архива
|
||||
|
||||
#### Scenario: Возвращение карты
|
||||
|
||||
- **WHEN** пользователь возвращает карту из архива
|
||||
- **THEN** она снова видна в обычном списке с сохранёнными данными и допускает финансовые действия по общим правилам
|
||||
|
||||
### Requirement: Месячный журнал и сводка расходов по категориям
|
||||
|
||||
Система SHALL показывать историю и месячную сводку выбранной карты, а также
|
||||
месячную сводку всех банковских карт пользователя. По умолчанию выбирается
|
||||
текущий месяц Europe/Moscow; пользователь SHALL иметь возможность явно выбрать
|
||||
другой месяц. История карты и её сводка MUST использовать один выбранный месяц.
|
||||
Операции SHALL отображаться по убыванию календарной даты со стабильным порядком
|
||||
внутри даты при повторном чтении неизменившихся данных и доступом к следующим
|
||||
страницам истории.
|
||||
|
||||
Сводка MUST показывать расходы, возвраты и итог после возвратов, а также
|
||||
соответствующие суммы по категориям. Расход входит в месяц собственной даты,
|
||||
возврат — в месяц даты возврата, даже если покупка была раньше.
|
||||
Итог MUST равняться расходам минус возвраты и может быть отрицательным.
|
||||
Начальные остатки, пополнения и переводы MUST NOT включаться в эти суммы.
|
||||
|
||||
Сводка MUST учитывать все подходящие не удалённые записи, а не только
|
||||
текущую страницу истории. «Все карты» MUST включать активные и архивные карты
|
||||
владельца. Выбор месяца и охвата MUST NOT изменять текущие балансы.
|
||||
При отсутствии расходов и возвратов SHALL показываться известные нули.
|
||||
|
||||
#### Scenario: Месяц по умолчанию и переключение
|
||||
|
||||
- **WHEN** пользователь открывает карту в сентябре по Europe/Moscow и затем выбирает август
|
||||
- **THEN** сначала история и сводка относятся к сентябрю, после выбора обе относятся к августу, а текущий баланс остаётся прежним
|
||||
|
||||
#### Scenario: Сводка расходов и переводов
|
||||
|
||||
- **WHEN** в месяце сохранены расходы «Кафе» 500 ₽ и «Такси» 300 ₽, пополнение 2 000 ₽ и перевод 1 000 ₽ без возвратов
|
||||
- **THEN** сводка показывает расходы 800 ₽, возвраты 0 ₽, итог 800 ₽ и категории 500 ₽ и 300 ₽ без включения пополнения или перевода
|
||||
|
||||
#### Scenario: Возврат покупки прошлого месяца
|
||||
|
||||
- **WHEN** расход «Покупки» 1 000 ₽ совершён в августе, возврат 600 ₽ — в сентябре и других операций этих видов нет
|
||||
- **THEN** август показывает расходы 1 000 ₽, возвраты 0 ₽ и итог 1 000 ₽, а сентябрь — расходы 0 ₽, возвраты 600 ₽ и итог −600 ₽ в той же категории
|
||||
|
||||
#### Scenario: Все карты включают архивные
|
||||
|
||||
- **WHEN** расходы активной A за месяц составили 800 ₽, архивной B — 200 ₽, а между ними ранее был перевод 1 000 ₽
|
||||
- **THEN** общая сводка показывает расходы 1 000 ₽ и не добавляет перевод, независимо от переключателя архива в списке карт
|
||||
|
||||
#### Scenario: Несколько страниц и равные даты
|
||||
|
||||
- **WHEN** операции выбранного месяца занимают несколько страниц и часть операций имеет одинаковую дату
|
||||
- **THEN** пользователь может просмотреть все страницы в стабильном порядке, а сводка и баланс учитывают все соответствующие записи независимо от открытой страницы
|
||||
|
||||
### Requirement: Изоляция пользовательских финансовых данных
|
||||
|
||||
Карты, категории, операции и связи между ними MUST принадлежать локальному
|
||||
пользователю WealthLedger. Система MUST предоставлять чтение, создание,
|
||||
изменение, удаление, архив и восстановление только владельцу с действующей
|
||||
сессией. Идентификатор сам по себе MUST NOT подтверждать доступ.
|
||||
|
||||
Проверка владельца MUST распространяться на каждую упомянутую карту,
|
||||
категорию и операцию, включая обе стороны перевода, исходный расход возврата
|
||||
и новые связи при редактировании. Отказ MUST NOT раскрывать чужие финансовые
|
||||
сведения или менять данные любой стороны. Сводки MUST включать только
|
||||
данные текущего пользователя.
|
||||
|
||||
#### Scenario: Нет действующей сессии
|
||||
|
||||
- **WHEN** пользователь без действующей сессии пытается прочитать карты, сводку или изменить финансовые данные
|
||||
- **THEN** система отказывает в доступе без раскрытия и изменения сохранённых данных
|
||||
|
||||
#### Scenario: Чужая карта или категория
|
||||
|
||||
- **WHEN** пользователь обращается к чужой карте или пытается назначить своей операции чужую категорию
|
||||
- **THEN** система отклоняет действие, не раскрывает чужие сведения и не меняет баланс, историю или категории
|
||||
|
||||
#### Scenario: Чужая связь перевода или возврата
|
||||
|
||||
- **WHEN** пользователь подставляет чужую карту в перевод или чужой расход в возврат при создании либо редактировании
|
||||
- **THEN** система отклоняет всё действие без сохранения частичного результата и без раскрытия данных другого пользователя
|
||||
|
||||
#### Scenario: Чужие исправления и архив
|
||||
|
||||
- **WHEN** пользователь пытается изменить или удалить чужую операцию, переименовать чужую категорию либо архивировать или восстановить чужую карту
|
||||
- **THEN** система отказывает и сохраняет все данные владельца без изменений
|
||||
|
||||
### Requirement: Согласованные изменения и защита повторной отправки
|
||||
|
||||
Успешное финансовое изменение MUST согласованно сохранять операции, связи,
|
||||
денежное влияние и пересчитанные результаты для всех затронутых карт.
|
||||
Система MUST NOT подтверждать успех несохранённого действия или оставлять
|
||||
частичный результат. Независимые одновременные операции MUST сохранять
|
||||
влияние каждой успешной записи, не затирая друг друга.
|
||||
|
||||
Повторная отправка одного намерения создать карту или операцию с тем же
|
||||
содержанием MUST приводить не более чем к одному соответствующему результату
|
||||
и однократному учёту начального остатка или денежного влияния операции.
|
||||
Иное содержание при повторном использовании того же намерения MUST приводить
|
||||
к конфликту без изменения данных. После исправления или удаления результата
|
||||
поздняя повторная отправка исходного создания MUST NOT восстанавливать
|
||||
старые значения, удалённую запись или её денежное влияние.
|
||||
Ответ на повтор MUST показывать актуальное состояние ранее созданной карты
|
||||
или операции либо сообщать, что операция удалена, не выдавая старые данные
|
||||
за текущий результат. Уже сохранённое намерение MUST NOT проверяться
|
||||
как новая финансовая запись и повторно расходовать доступную сумму возврата.
|
||||
Архивирование ранее созданной карты MUST NOT превращать повтор её создания
|
||||
в создание другой карты или возвращение из архива.
|
||||
|
||||
Редактирование или удаление существующей операции и изменение карты,
|
||||
основанные на её устаревшем состоянии, MUST завершаться понятным конфликтом
|
||||
без перезаписи более свежих изменений. Повторное удаление уже удалённой
|
||||
операции не считается перезаписью и остаётся безрезультатным действием.
|
||||
Проверки суммы возвратов, состояния архива, дат и других ограничений
|
||||
MUST сохраняться при одновременных действиях.
|
||||
|
||||
#### Scenario: Повтор после потерянного ответа
|
||||
|
||||
- **WHEN** создание расхода 100 ₽ успешно сохранено, ответ потерян и пользователь повторяет то же намерение с тем же содержанием
|
||||
- **THEN** история содержит один расход, баланс изменён один раз, а повтор не создаёт дубликат
|
||||
|
||||
#### Scenario: Повтор создания карты после архивации
|
||||
|
||||
- **WHEN** карта с начальным остатком 1 000 ₽ создана, ответ потерян, карта затем архивирована и приходит повтор её исходного создания
|
||||
- **THEN** система показывает актуальную архивную карту, не создаёт вторую карту, не дублирует начальный остаток и не возвращает карту из архива
|
||||
|
||||
#### Scenario: Повтор полного возврата
|
||||
|
||||
- **WHEN** возврат 500 ₽ полностью покрывает расход 500 ₽, успешно сохранён, но ответ потерян и пользователь повторяет то же намерение создания возврата
|
||||
- **THEN** система возвращает сохранённый возврат без новой записи, повторного изменения баланса или ошибки о превышении суммы покупки
|
||||
|
||||
#### Scenario: Изменённое содержание того же намерения
|
||||
|
||||
- **WHEN** после сохранения расхода 100 ₽ повтор того же намерения содержит сумму 200 ₽
|
||||
- **THEN** система сообщает о конфликте и сохраняет исходный результат без дополнительной операции
|
||||
|
||||
#### Scenario: Позднее создание после исправления или удаления
|
||||
|
||||
- **WHEN** сохранённый расход 100 ₽ исправлен на 150 ₽ или удалён, после чего приходит повтор его исходного создания
|
||||
- **THEN** система показывает актуальный расход 150 ₽ либо сообщает об удалении; прежняя сумма или удалённая запись не восстанавливаются и дополнительного денежного влияния нет
|
||||
|
||||
#### Scenario: Два независимых расхода
|
||||
|
||||
- **WHEN** при балансе 1 000 ₽ расходы 100 ₽ и 200 ₽ успешно сохраняются одновременно как разные намерения
|
||||
- **THEN** история содержит оба расхода, а баланс составляет 700 ₽
|
||||
|
||||
#### Scenario: Два возврата превышают покупку вместе
|
||||
|
||||
- **WHEN** для расхода 1 000 ₽ одновременно сохраняются независимые возвраты 600 ₽ и 600 ₽
|
||||
- **THEN** система не допускает общей сохранённой суммы 1 200 ₽: конфликтующее действие отклоняется без частичного результата
|
||||
|
||||
#### Scenario: Исправление по устаревшим данным
|
||||
|
||||
- **WHEN** операция или карта изменилась после того, как пользователь начал её редактирование, а он отправляет изменение прежнего состояния
|
||||
- **THEN** система сообщает о конфликте, сохраняет более свежие данные и позволяет получить актуальное состояние для нового исправления
|
||||
|
||||
#### Scenario: Удаление по устаревшим данным
|
||||
|
||||
- **WHEN** пользователь пытается удалить существующую операцию, которую другой успешный запрос уже изменил
|
||||
- **THEN** система отклоняет устаревшее удаление без потери более свежего исправления
|
||||
|
||||
#### Scenario: Архивирование одновременно с финансовым изменением
|
||||
|
||||
- **WHEN** архивирование карты пересекается с созданием или исправлением затрагивающей её операции
|
||||
- **THEN** результаты соответствуют последовательному выполнению допустимых действий и не допускают финансовую запись, выполненную после вступившего в силу архива
|
||||
|
||||
### Requirement: Сохранённый ручной учёт переживает повторный вход и перезапуск
|
||||
|
||||
Успешно сохранённые карты, начальные остатки, даты начала, операции,
|
||||
категории, архивное состояние и связи возвратов и переводов MUST оставаться
|
||||
доступными владельцу после обновления страницы, выхода, повторного входа
|
||||
и обычного перезапуска приложения. Эти действия MUST NOT создавать
|
||||
дубликаты, восстанавливать удалённые операции или сбрасывать балансы.
|
||||
Защита повторной отправки сохранённых намерений MUST сохранять действие
|
||||
после этих переходов.
|
||||
|
||||
#### Scenario: Повторный вход
|
||||
|
||||
- **WHEN** пользователь выходит и снова входит после сохранения карты с начальным остатком 1 000 ₽ и расходом 100 ₽
|
||||
- **THEN** он видит ту же карту, дату начала, расход с его текущей категорией и баланс 900 ₽
|
||||
|
||||
#### Scenario: Перезапуск с переводом, возвратом и архивом
|
||||
|
||||
- **WHEN** приложение перезапускается после сохранения переводов, связанных возвратов, переименования категорий и архивирования карты
|
||||
- **THEN** владелец получает прежние связи, названия, архивное состояние и рассчитанные суммы без дубликатов или потери истории
|
||||
|
||||
#### Scenario: Повтор создания после перезапуска
|
||||
|
||||
- **WHEN** после перезапуска повторяется намерение создания ранее сохранённой, а затем удалённой операции
|
||||
- **THEN** удалённая операция и её денежное влияние не восстанавливаются
|
||||
|
||||
### Requirement: Доступные формы и различимые состояния без ложного успеха
|
||||
|
||||
Раздел SHALL различать отсутствие карт, отсутствие операций за месяц,
|
||||
известный нулевой баланс, загрузку и ошибку получения данных. Ошибка
|
||||
MUST NOT подменяться пустым списком или нулевой суммой. Пользователь SHALL
|
||||
иметь возможность повторить неуспешную загрузку.
|
||||
|
||||
Формы карт, операций, исправлений и категорий MUST иметь доступные названия
|
||||
полей и действий, управление с клавиатуры и понятные ошибки, связанные
|
||||
с соответствующим полем или действием. Обязательность, направление денег
|
||||
и ошибки MUST NOT передаваться только цветом.
|
||||
|
||||
Во время сохранения, ошибки и конфликта введённый черновик MUST оставаться
|
||||
доступным до явного закрытия или успешного завершения. Пока результат
|
||||
сохранения не подтверждён, система MUST NOT показывать введённую сумму
|
||||
как подтверждённый новый баланс или выдавать действие за успешное.
|
||||
При неопределённом результате после потери ответа повтор SHALL использовать
|
||||
защиту того же намерения создания. После подтверждённого изменения
|
||||
отображаемые история, балансы и сводки MUST согласовываться с сохранёнными
|
||||
данными всех затронутых карт.
|
||||
|
||||
#### Scenario: Пустой список
|
||||
|
||||
- **WHEN** раздел успешно загружен и у пользователя ещё нет карт
|
||||
- **THEN** система показывает штатное пустое состояние с доступным действием добавления карты
|
||||
|
||||
#### Scenario: Нет операций при ненулевом балансе
|
||||
|
||||
- **WHEN** в выбранном месяце нет операций, но текущий баланс карты составляет 10 000 ₽
|
||||
- **THEN** история сообщает об отсутствии операций за месяц, сводка содержит нули, а текущий баланс остаётся 10 000 ₽
|
||||
|
||||
#### Scenario: Ошибка загрузки
|
||||
|
||||
- **WHEN** загрузка карт, истории или сводки завершается ошибкой
|
||||
- **THEN** система показывает ошибку и действие повтора, не выдавая её за отсутствие данных или нулевой результат
|
||||
|
||||
#### Scenario: Доступный ввод и проверка
|
||||
|
||||
- **WHEN** пользователь управляет формой с клавиатуры или средством чтения экрана и вводит некорректную сумму
|
||||
- **THEN** названия полей и действий доступны, ошибка суммы определима без цвета, а введённые значения остаются для исправления
|
||||
|
||||
#### Scenario: Ожидание и неопределённый результат
|
||||
|
||||
- **WHEN** пользователь отправляет перевод и ответ ещё не получен либо потерян
|
||||
- **THEN** черновик сохраняется, неподтверждённые балансы не выдаются за сохранённые, а повтор того же создания не порождает второй перевод
|
||||
|
||||
#### Scenario: Ошибка или конфликт исправления
|
||||
|
||||
- **WHEN** сохранение исправления отклоняется из-за ошибки или устаревшего состояния
|
||||
- **THEN** пользователь видит причину, сохраняет черновик и может получить актуальные данные для следующего действия без перезаписи чужого успешного изменения
|
||||
@@ -8,21 +8,21 @@
|
||||
|
||||
### Requirement: Каждый change содержит полный Delivery Map
|
||||
|
||||
Каждый versioned change MUST оценивать delivery areas `frontend`, `backend`, `specs` и `docs` и для каждой области указывать `affected` либо `N/A` с конкретной причиной. `specs` SHALL быть affected для каждого versioned SDD change. `docs` SHALL быть affected для каждой поставленной пользовательской фичи; для технического или процессного change она MAY быть `N/A` только когда фактическая устойчивая документация не изменяется. `frontend` и `backend` SHALL быть affected только когда меняется их поведение, контракт, реализация или обязательный локальный gate. Пустой коммит MUST NOT использоваться для имитации участия незатронутого репозитория.
|
||||
Каждый legacy `wealth-ledger-sdd` change MUST оценивать delivery areas `frontend`, `backend`, `specs` и `docs` в Design Delivery Map и для каждой области указывать `affected` либо `N/A` с конкретной причиной. Для `wealth-ledger-sdd-v2` Design оценивает implementation impact `main`, `frontend`, `backend` (и `qa`, только если существует repo), а Docs Impact отдельно оценивает `docs`; `specs` владеет SDD artifacts и native sync/archive без фиктивной implementation task. Для обоих workflows `docs` SHALL быть affected для каждой поставленной пользовательской фичи; для технического или процессного change она MAY быть `N/A` только когда фактическая устойчивая документация не изменяется. `frontend` и `backend` SHALL быть affected только когда меняется их поведение, контракт, реализация или обязательный локальный gate. Пустой коммит MUST NOT использоваться для имитации участия незатронутого репозитория.
|
||||
|
||||
#### Scenario: Фича затрагивает клиент и API
|
||||
|
||||
- **WHEN** change меняет пользовательский поток и соответствующее API-поведение
|
||||
- **THEN** Delivery Map отмечает frontend, backend, specs и docs как affected и описывает ожидаемый результат каждой области без реализации по файлам
|
||||
- **THEN** legacy Delivery Map отмечает frontend, backend, specs и docs как affected; v2 Design отмечает frontend/backend, а Docs Impact — docs и отдельный specs artifact не превращается в пустую task
|
||||
|
||||
#### Scenario: Область не затронута
|
||||
|
||||
- **WHEN** поведение, контракт, реализация, guardrails и фактическая документация области не меняются
|
||||
- **THEN** Delivery Map содержит проверяемую причину `N/A`, а delivery не требует пустого коммита этого репозитория
|
||||
- **THEN** соответствующий impact artifact содержит проверяемую причину `N/A`, а delivery не требует пустого коммита этого репозитория
|
||||
|
||||
### Requirement: Частичная multi-repository delivery не считается завершённой
|
||||
|
||||
Change MUST оставаться active, пока каждая affected область не реализована и не прошла свои обязательные локальные gates. Успех одной области MUST NOT позволять архивировать change, если другая affected область отсутствует, не проверена либо не соответствует согласованному поведению. После archive change MUST NOT получить terminal status `done`, пока фактические результаты всех четырёх delivery areas не представлены в Completion Evidence.
|
||||
Change MUST оставаться active, пока каждая affected область не реализована и не прошла свои обязательные локальные gates. Успех одной области MUST NOT позволять архивировать change, если другая affected область отсутствует, не проверена либо не соответствует согласованному поведению. После archive change MUST NOT получить terminal status `done`, пока фактические результаты всех применимых delivery areas выбранной schema не представлены в Completion Evidence.
|
||||
|
||||
#### Scenario: Backend готов, а frontend ещё не реализован
|
||||
|
||||
@@ -36,12 +36,12 @@ Change MUST оставаться active, пока каждая affected обла
|
||||
|
||||
### Requirement: Completion Evidence содержит только наблюдённые результаты
|
||||
|
||||
Completion Evidence MUST создаваться после выполнения работы и MUST связывать canonical change с commit identifier каждого изменённого Git-репозитория, точными выполненными non-mutating verification commands и их наблюдёнными результатами. Для каждой области с `N/A` evidence SHALL сохранять согласованную причину отсутствия изменения. Planned commands, ожидаемые результаты, незавершённые checklist items и предполагаемые commit identifiers MUST NOT представляться как completion evidence.
|
||||
Completion Evidence MUST создаваться после выполнения работы и MUST связывать canonical change с commit identifier каждого изменённого Git-репозитория, точными выполненными non-mutating verification commands и их наблюдёнными результатами. Для каждой оценённой области с `N/A` evidence SHALL сохранять согласованную причину отсутствия изменения. Planned commands, ожидаемые результаты, незавершённые checklist items и предполагаемые commit identifiers MUST NOT представляться как completion evidence.
|
||||
|
||||
#### Scenario: Change готовится к terminal status
|
||||
|
||||
- **WHEN** implementer заявляет, что multi-repository change завершён
|
||||
- **THEN** reviewer может сопоставить Delivery Map с фактическими commits, результатами gates и причинами `N/A` для всех четырёх областей
|
||||
- **THEN** reviewer может сопоставить impact artifacts выбранной schema с фактическими commits, результатами gates и причинами `N/A` для всех оценённых областей
|
||||
|
||||
#### Scenario: Команда ещё не была выполнена
|
||||
|
||||
@@ -64,7 +64,7 @@ Workspace SHALL предоставлять один non-mutating локальн
|
||||
|
||||
### Requirement: Каждая задача объявляет единственную delivery area
|
||||
|
||||
Каждый checkbox Tasks MUST объявлять ровно одного owner из `frontend`, `backend`, `specs`, `docs` или `workspace`. Implementation work, изменяющая несколько Git-репозиториев, MUST быть разделена на repository-owned tasks; `workspace` SHALL использоваться только для cross-area orchestration или verification без владения repository implementation. Для каждой `affected` area Tasks MUST содержать её implementation work и canonical local gate, а task ownership MUST соответствовать утверждённому Delivery Map.
|
||||
Каждый checkbox legacy Tasks MUST объявлять ровно одного owner из `frontend`, `backend`, `specs`, `docs` или `workspace`; `workspace` SHALL использоваться только для cross-area orchestration или verification без владения repository implementation. Каждый checkbox v2 Tasks MUST объявлять ровно одного repository owner из `main`, `frontend`, `backend`, `docs`, `specs` или существующего `qa`; `main` владеет только workspace/Compose/release/configuration, `specs` — genuine repository maintenance, `docs` — approved Docs Impact, `qa` — только реальная automation delivery, но не manual acceptance checkbox. Implementation work, изменяющая несколько Git-репозиториев, MUST быть разделена на repository-owned tasks. Для каждой `affected` implementation area Tasks MUST содержать её implementation work и canonical local gate, а task ownership MUST соответствовать утверждённым impact artifacts выбранной schema.
|
||||
|
||||
#### Scenario: Change затрагивает frontend и backend
|
||||
|
||||
@@ -74,16 +74,16 @@ Workspace SHALL предоставлять один non-mutating локальн
|
||||
#### Scenario: Выполняется общий verification gate
|
||||
|
||||
- **WHEN** задача только оркестрирует проверку нескольких affected areas и не изменяет их implementation
|
||||
- **THEN** task использует owner `workspace` и не подменяет repository-owned implementation или canonical gates
|
||||
- **THEN** legacy task MAY использовать `workspace`; v2 использует `main` только при реальном workspace-owned outcome, не маскируя независимую Validation под implementation checkbox
|
||||
|
||||
#### Scenario: Task ссылается на N/A area
|
||||
|
||||
- **WHEN** checkbox объявляет owner области, которая имеет статус `N/A` в Delivery Map
|
||||
- **THEN** planning validation завершается ошибкой и указывает несогласованную delivery area
|
||||
- **THEN** planning validation завершается ошибкой и указывает несогласованную delivery area выбранной schema
|
||||
|
||||
### Requirement: Tasks трассируются к утверждённым требованиям
|
||||
|
||||
Каждый checkbox Tasks MUST содержать coverage reference: одну или несколько пар из точного capability path и точного Requirement heading либо `N/A` с конкретной причиной для работы, которая не реализует spec-level behavior. Причина `N/A` MUST объяснять отсутствие самостоятельного observable behavior, техническую необходимость задачи и её связь с утверждённым Design или delivery; однословная категория или неразрешённый placeholder не являются достаточной причиной. Каждый Requirement из Delta Specs MUST быть покрыт хотя бы одной task. Planning validation MUST отклонять отсутствующее покрытие, ссылку на несуществующий capability или Requirement, пустую причину `N/A` и неразрешённый placeholder. Содержательная достаточность непустой причины SHALL оцениваться semantic approval и independent review и MUST NOT выдаваться за детерминированный hard guardrail. Traceability SHALL использовать нативную идентичность requirement и MUST NOT вводить параллельный namespace собственных requirement IDs.
|
||||
Каждый checkbox Tasks MUST содержать coverage reference: одну или несколько пар из точного capability path и точного Requirement heading либо `N/A` с конкретной причиной для работы, которая не реализует spec-level behavior. Причина `N/A` MUST объяснять отсутствие самостоятельного observable behavior, техническую необходимость задачи и её связь с утверждённым Design или delivery; для v2 также допустима связь с System Analysis или Docs Impact. Однословная категория или неразрешённый placeholder не являются достаточной причиной. Каждый Requirement из Delta Specs MUST быть покрыт хотя бы одной task; для v2 допустимо явное объяснение отсутствия implementation work. Planning validation MUST отклонять отсутствующее покрытие, ссылку на несуществующий capability или Requirement, пустую причину `N/A` и неразрешённый placeholder. Содержательная достаточность непустой причины SHALL оцениваться semantic approval и independent review и MUST NOT выдаваться за детерминированный hard guardrail. Traceability SHALL использовать нативную идентичность requirement и MUST NOT вводить параллельный namespace собственных requirement IDs.
|
||||
|
||||
#### Scenario: Delta requirement покрыт реализацией
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# online-instrument-information Specification
|
||||
|
||||
## Purpose
|
||||
Определяет online-only получение справочных сведений и результатов поиска инструментов отдельно от сохранённых личных финансовых данных.
|
||||
|
||||
## 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** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
### Requirement: SDD-артефакты имеют непересекающиеся полномочия
|
||||
|
||||
Proposal SHALL выражать причину, намерение, границы и ожидаемый эффект change языком владельца и MUST NOT содержать решения по технологиям, структуре кода, файлам, API или зависимостям. Delta Specs SHALL определять только добавленное, изменённое или удалённое наблюдаемое поведение и инварианты системы без описания изменений кода. Design SHALL объяснять архитектурный способ выполнения утверждённых Specs, существенные решения, риски и альтернативы крупными частями и MUST NOT становиться построчной инструкцией реализации. Применимые UI/UX-решения и визуальные материалы SHALL входить в единый canonical Design и MUST NOT образовывать отдельный planning authority или отдельный UI/UX approval. Tasks SHALL быть implementation checklist, достаточным для выполнения Design и Specs, и MUST NOT содержать ещё не наблюдённые completion results, commit identifiers или post-archive bookkeeping. Completion Evidence SHALL фиксироваться только после фактической реализации и верификации и MUST NOT подменять planning artifact. Концептуальные материалы и Discovery SHALL оставаться входным контекстом и MUST NOT считаться утверждёнными OpenSpec artifacts.
|
||||
Proposal SHALL выражать причину, намерение, границы и ожидаемый эффект change языком владельца и MUST NOT содержать решения по технологиям, структуре кода, файлам, API или зависимостям. Delta Specs SHALL определять только добавленное, изменённое или удалённое наблюдаемое поведение и инварианты системы без описания изменений кода. В `wealth-ledger-sdd-v2` System Analysis SHALL определять межсистемные потоки, contracts и semantics до Design; Design SHALL объяснять внутреннюю реализацию approved behavior/contracts, риски и альтернативы, не становясь построчной инструкцией. QA Analysis SHALL определять независимую system verification, не предписывая разработчикам internal tests. Docs Impact SHALL планировать только durable AS-IS knowledge для `docs/`. Применимые UI/UX-решения и визуальные материалы SHALL входить в единый canonical Design и MUST NOT образовывать отдельный planning authority или отдельный UI/UX approval. Tasks SHALL быть implementation checklist из утверждённых upstream artifacts и MUST NOT содержать ещё не наблюдённые completion results, commit identifiers или post-archive bookkeeping. Completion Evidence SHALL фиксироваться только после фактической реализации и верификации и MUST NOT подменять planning artifact. Концептуальные материалы и Discovery SHALL оставаться входным контекстом и MUST NOT считаться утверждёнными OpenSpec artifacts.
|
||||
|
||||
#### Scenario: Product Owner формулирует Proposal
|
||||
|
||||
@@ -16,7 +16,7 @@ Proposal SHALL выражать причину, намерение, границ
|
||||
|
||||
#### Scenario: Агент подготавливает Tasks
|
||||
|
||||
- **WHEN** Proposal, Delta Specs и Design уже утверждены
|
||||
- **WHEN** upstream artifacts выбранной schema утверждены
|
||||
- **THEN** Tasks перечисляет конкретные действия реализации и проверки, но не заявляет будущие результаты уже выполненными
|
||||
|
||||
#### Scenario: Владелец согласует визуальный результат
|
||||
@@ -33,26 +33,31 @@ Proposal SHALL выражать причину, намерение, границ
|
||||
|
||||
### Requirement: Artifact graph применяет последовательные approval gates
|
||||
|
||||
Versioned SDD change SHALL проходить последовательность `Proposal → Delta Specs → Design → Tasks → Apply`. AI-агент MAY подготовить черновик текущего разрешённого артефакта, но MUST NOT считать его утверждённым самостоятельно или создавать downstream artifact до явного решения Change Owner. Для change, определяющего продуктовое поведение, Change Owner SHALL быть Product Owner; для технического или процессного change — Engineering Owner. Каждый из четырёх planning artifacts MUST существовать и быть утверждён до Apply.
|
||||
Каждый change SHALL следовать schema из своего `.openspec.yaml`: существующие active `wealth-ledger-sdd` сохраняют `Proposal → Delta Specs → Design → Tasks → Apply`, а новые `wealth-ledger-sdd-v2` используют `Proposal → Delta Specs → System Analysis → (Design + QA Analysis) → Docs Impact → Tasks → Apply`. В v2 Design и QA Analysis MAY подготавливаться параллельно после approval System Analysis; Docs Impact требует approvals обоих. `skip_specs: true` явно обозначает отсутствие behavior delta, а не фиктивный spec. AI-агент MAY подготовить черновик текущего разрешённого артефакта, но MUST NOT считать его утверждённым самостоятельно или создавать downstream artifact до явного решения Change Owner. Для change, определяющего продуктовое поведение, Change Owner SHALL быть Product Owner; для технического или процессного change — Engineering Owner. Каждый применимый planning artifact выбранной schema MUST существовать и быть явно утверждён до Apply; существующие approvals legacy changes MUST NOT инвалидироваться только из-за смены default schema.
|
||||
|
||||
#### Scenario: Proposal ещё не утверждён
|
||||
|
||||
- **WHEN** AI-агент завершил черновик Proposal, но Change Owner не дал явного согласия
|
||||
- **THEN** Delta Specs, Design и Tasks остаются недоступны для создания
|
||||
- **THEN** downstream artifacts выбранной schema остаются недоступны для создания
|
||||
|
||||
#### Scenario: Delta Specs утверждены
|
||||
|
||||
- **WHEN** Change Owner явно подтверждает Delta Specs после утверждённого Proposal
|
||||
- **THEN** Design становится следующим разрешённым planning artifact, а Tasks и Apply остаются заблокированы
|
||||
- **THEN** для legacy разрешён Design, для v2 — System Analysis; Tasks и Apply остаются заблокированы
|
||||
|
||||
#### Scenario: Active change переживает смену default schema
|
||||
|
||||
- **WHEN** active change закреплён за `wealth-ledger-sdd` и default меняется на `wealth-ledger-sdd-v2`
|
||||
- **THEN** он сохраняет прежний artifact graph и approvals без создания фиктивных v2 artifacts
|
||||
|
||||
### Requirement: Изменение upstream artifact требует каскадной проверки
|
||||
|
||||
Изменение утверждённого Proposal MUST инициировать повторную проверку Delta Specs, Design и Tasks; изменение утверждённых Delta Specs MUST инициировать повторную проверку Design и Tasks; изменение утверждённого Design MUST инициировать повторную проверку Tasks. Изменение Tasks MUST NOT неявно менять scope, наблюдаемое поведение или архитектурные решения upstream artifacts; при таком обнаружении workflow SHALL вернуть работу к самому раннему затронутому upstream artifact и его approval gate. Если несоответствие обнаружено во время Design или Apply, downstream-работа MUST остановиться, а Change Owner MUST получить наблюдаемое основание возврата и решения, требуемые на выбранном уровне. Механическое исправление синтаксиса или формата, которое не меняет утверждённые намерение, поведение, архитектурные решения или состав задач, MUST пройти повторную structural validation и MUST NOT само по себе инвалидировать approvals. Исправление Markdown heading, опечатки без изменения нормативного смысла или выравнивания таблицы MAY считаться механическим; изменение формулировки Requirement, добавление или удаление Scenario, изменение выбранного архитектурного решения либо planned Verification задачи MUST NOT считаться механическим.
|
||||
Семантическое изменение утверждённого artifact MUST возвращаться к самой ранней затронутой planning boundary и инициировать повторную проверку и approval только её зависимых downstream artifacts согласно выбранной schema. Для legacy Proposal → Delta Specs → Design → Tasks; для v2 intent/scope → Proposal, observable behavior → Specs, systems/sequences/contracts/mappings/inter-system errors/semantic observability → System Analysis, internal implementation/storage/technical observability/developer tests → Design, system verification/test data/automation → QA Analysis, durable AS-IS/ADR → Docs Impact, decomposition/ownership/dependencies → Tasks. Изменение Tasks MUST NOT неявно менять upstream semantics. Если несоответствие обнаружено во время Design или Apply, downstream-работа MUST остановиться, а Change Owner MUST получить наблюдаемое основание возврата. Механическое исправление синтаксиса или формата без изменения смысла MUST пройти повторную structural validation и MUST NOT само по себе инвалидировать approvals. Исправление Markdown heading, опечатки без изменения нормативного смысла или выравнивания таблицы MAY считаться механическим; изменение Requirement/Scenario, contract, sequence, архитектурного решения, QA expected result, docs target либо planned Verification задачи MUST NOT считаться механическим.
|
||||
|
||||
#### Scenario: После Design изменилось намерение
|
||||
|
||||
- **WHEN** владелец меняет scope или ожидаемый эффект Proposal
|
||||
- **THEN** зависимые Delta Specs, Design и Tasks теряют прежнее подтверждение до повторной согласованной проверки
|
||||
- **THEN** зависимые artifacts выбранной schema теряют прежнее подтверждение до повторной согласованной проверки
|
||||
|
||||
#### Scenario: Task вводит новое поведение
|
||||
|
||||
@@ -62,7 +67,7 @@ Versioned SDD change SHALL проходить последовательност
|
||||
#### Scenario: Apply выявляет архитектурное расхождение
|
||||
|
||||
- **WHEN** во время реализации обнаружено, что утверждённый Design не позволяет выполнить Specs без другого архитектурного решения
|
||||
- **THEN** Apply останавливается, Tasks теряет прежнее подтверждение, а change возвращается к Design с наблюдаемым основанием и вариантами решения
|
||||
- **THEN** Apply останавливается, затронутые downstream approvals теряют силу, а change возвращается к Design с наблюдаемым основанием и вариантами решения
|
||||
|
||||
#### Scenario: Исправляется только структурный формат
|
||||
|
||||
@@ -107,33 +112,28 @@ Canonical store и policy расположения artifacts SHALL иметь do
|
||||
- **WHEN** project-owned OpenSpec entrypoint допускает выбор nearest-root или global-default
|
||||
- **THEN** verifier завершается ошибкой и указывает non-compliant entrypoint
|
||||
|
||||
### Requirement: OpenSpec tooling имеет проверяемый version contract
|
||||
### Requirement: OpenSpec tooling проверяется по совместимости
|
||||
|
||||
SDD workflow MUST объявлять одну текущую поддерживаемую версию OpenSpec CLI в canonical version contract и проверять фактическую версию до команд, читающих или изменяющих canonical change. Normative checks и проверяемые generated markers MUST получать ожидаемую версию из этого контракта и MUST NOT независимо закреплять конкретный version literal. Несовместимая либо неопределённая версия MUST давать actionable blocking result и MUST NOT приводить к молчаливому изменению artifact graph, templates или project-owned entrypoints. Публикация новой stable версии MUST NOT автоматически менять поддерживаемую версию проекта. Upgrade SHALL изменять version contract только после изолированной проверки совместимости schema, agent workflow, canonical store и hard blockers и SHALL сохранять историческое evidence с фактически использованными ранее версиями.
|
||||
SDD workflow MUST проверять доступный OpenSpec CLI на совместимость с выбранной schema и canonical store без закрепления одной версии CLI. Перед изменением workflow или обновлением CLI SHALL выполняться native validation обеих project-local schemas, затронутых changes и canonical specs; artifact graph, agent workflow, store selection и blocking gates MUST сохранять согласованную семантику. Несовместимый CLI MUST давать actionable blocking result и MUST NOT приводить к молчаливому изменению artifacts или project-owned entrypoints. Фактически использованная версия CLI SHALL сохраняться в observed verification evidence, а не в отдельном version contract.
|
||||
|
||||
#### Scenario: Версия CLI соответствует контракту
|
||||
#### Scenario: Доступный CLI совместим
|
||||
|
||||
- **WHEN** агент начинает работу с canonical change поддерживаемой версией OpenSpec
|
||||
- **THEN** workflow продолжает работу с объявленной схемой и canonical store
|
||||
- **WHEN** проверки выбранной schema и canonical store проходят на установленном OpenSpec CLI
|
||||
- **THEN** workflow продолжает работу без требования совпадения с закреплённым номером версии
|
||||
|
||||
#### Scenario: Версия CLI не соответствует контракту
|
||||
#### Scenario: CLI нарушает совместимость
|
||||
|
||||
- **WHEN** доступная версия OpenSpec не совпадает с объявленным совместимым контрактом
|
||||
- **THEN** локальная preflight-проверка блокирует SDD-операцию и сообщает ожидаемую и фактическую версии
|
||||
- **WHEN** после обновления CLI schema validation, разрешение store или blocking gates перестают работать
|
||||
- **THEN** зависимая SDD-операция останавливается с диагностикой нарушения до исправления несовместимости
|
||||
|
||||
#### Scenario: Опубликована новая stable версия
|
||||
#### Scenario: CLI обновлён
|
||||
|
||||
- **WHEN** upstream публикует новую stable версию, которая ещё не прошла project compatibility assessment
|
||||
- **THEN** текущий version contract остаётся действующим и новая версия не становится неявным требованием для разработки
|
||||
|
||||
#### Scenario: Подготовлен upgrade candidate
|
||||
|
||||
- **WHEN** Engineering Owner рассматривает смену поддерживаемой версии
|
||||
- **THEN** изолированная проверка подтверждает schema, agent workflow, store selection и hard blockers до изменения version contract
|
||||
- **WHEN** Engineering Owner обновляет OpenSpec CLI
|
||||
- **THEN** проверяются обе project-local schemas, затронутые changes, canonical specs и workflow, а фактическая версия и результаты сохраняются в verification evidence
|
||||
|
||||
### Requirement: Structural и semantic verification являются разными gates
|
||||
|
||||
OpenSpec structural validation SHALL проверять schema, artifact graph и формальную корректность specs, но MUST NOT считаться доказательством соответствия реализации. До terminal status change MUST пройти semantic verification реализации против Proposal, Delta Specs, Design и Tasks, а затем независимый read-only review. Если основной agent entrypoint semantic verification недоступен, workflow SHALL использовать versioned эквивалентную процедуру с теми же входными артефактами, категориями проверки и наблюдаемым evidence.
|
||||
OpenSpec structural validation SHALL проверять schema, artifact graph и формальную корректность specs, но MUST NOT считаться доказательством соответствия реализации или approval. До terminal status change MUST пройти semantic verification реализации против применимых approved artifacts выбранной schema, включая v2 System Analysis, QA Analysis и Docs Impact, а затем независимый read-only review. Если основной agent entrypoint semantic verification недоступен, workflow SHALL использовать versioned эквивалентную процедуру с теми же входными артефактами, категориями проверки и наблюдаемым evidence.
|
||||
|
||||
#### Scenario: Structural validation прошла без ошибок
|
||||
|
||||
@@ -146,7 +146,7 @@ OpenSpec structural validation SHALL проверять schema, artifact graph
|
||||
- **THEN** агент выполняет документированную эквивалентную read-only проверку и сохраняет её результат вместо пропуска gate
|
||||
|
||||
### Requirement: Archive блокируется незавершёнными tasks
|
||||
Project-owned archive workflow MUST отклонять archive, если отсутствует требуемый planning artifact, любой обязательный artifact не утверждён, `tasks.md` отсутствует или содержит unchecked task, semantic verification либо независимый read-only review не завершены успешно, или любая affected delivery area не реализована и не прошла обязательные pre-archive gates. Workflow MUST NOT предлагать confirmation override для этих blocking conditions. Post-archive bookkeeping MUST NOT появляться как unchecked task; он SHALL фиксироваться через детерминированное Completion Evidence после успешного archive, после чего связанная delivery может получить terminal status.
|
||||
Project-owned archive workflow MUST отклонять archive, если отсутствует требуемый выбранной schema planning artifact, любой обязательный artifact не утверждён, `tasks.md` отсутствует или содержит unchecked task, semantic verification либо независимый read-only review не завершены успешно, или любая affected delivery area не реализована и не прошла обязательные pre-archive gates. Для v2 также MUST быть подтверждены developer checks, независимая QA/system verification, semantic compatibility backend-generated OpenAPI/runtime interface с approved target contract при изменении API и применение Docs Impact так, чтобы canonical `docs/` отражали resulting AS-IS. Native OpenSpec sync/archive SHALL обновлять canonical behavior specs, не переписывая их implementation details; System Analysis и target contracts остаются в архивном change, а не становятся второй вечной копией current OpenAPI. Workflow MUST NOT предлагать confirmation override для этих blocking conditions. Post-archive bookkeeping MUST NOT появляться как unchecked task; он SHALL фиксироваться через детерминированное Completion Evidence после успешного archive, после чего связанная delivery может получить terminal status.
|
||||
|
||||
#### Scenario: Change имеет unchecked task
|
||||
- **WHEN** implementer запрашивает archive и остаётся хотя бы один unchecked task
|
||||
@@ -181,7 +181,7 @@ Project-owned archive workflow MUST отклонять archive, если отс
|
||||
|
||||
#### Scenario: Downstream artifacts ещё отсутствуют
|
||||
|
||||
- **WHEN** Proposal создан, а Delta Specs, Design и Tasks ещё не существуют
|
||||
- **WHEN** Proposal создан, а downstream artifacts выбранной schema ещё не существуют
|
||||
- **THEN** workflow сообщает результат доступных Proposal checks и не представляет отсутствующую full-change validation как успешную
|
||||
|
||||
### Requirement: Project schema сохраняет заметные самодостаточные правила
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# portfolio-value-history Specification
|
||||
|
||||
## Purpose
|
||||
Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек.
|
||||
|
||||
## 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** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений
|
||||
@@ -7,13 +7,13 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: Получение Portfolio выбранного Brokerage Account
|
||||
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
|
||||
Система 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, а обязательные upstream-данные успешно получены
|
||||
- **THEN** система отвечает `200` полным нормализованным Portfolio Snapshot с `isStale: false` и временем успешного получения финансового снимка
|
||||
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён
|
||||
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta`
|
||||
|
||||
#### Scenario: Недоступный Brokerage Account
|
||||
- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection
|
||||
@@ -94,144 +94,72 @@ Portfolio Snapshot является результатом наблюдения
|
||||
- **THEN** система передаёт её как строку `"-0.5"`
|
||||
|
||||
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||
Система MUST хранить последний полный успешный Portfolio Snapshot в SQLite
|
||||
с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный
|
||||
GET MUST читать сохранённые данные без ожидания T-Bank; missing/stale источники
|
||||
обновляются в фоне по пользовательскому запросу. Portfolio source свеж 60 секунд.
|
||||
Ошибка обновления MUST сохранять прежний полный Snapshot и его время без
|
||||
пятиминутного лимита fallback. Metadata MUST NOT определять успешность или
|
||||
свежесть финансового Snapshot; при её отсутствии используется доступный fallback.
|
||||
Система 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
|
||||
публиковаться в новом. Общие metadata и подтверждённое отсутствие инструмента
|
||||
сохраняются по UID/FIGI на 24 часа; ошибка lookup не подтверждает отсутствие.
|
||||
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента.
|
||||
|
||||
#### Scenario: Свежий Snapshot
|
||||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 секунд после успешного получения
|
||||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
|
||||
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Медленный lookup инструмента
|
||||
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
|
||||
- **THEN** система не задерживает локальный ответ ради lookup и показывает доступную identity; сохранённая успешная metadata используется последующим чтением
|
||||
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
|
||||
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity
|
||||
|
||||
#### Scenario: Fallback после временной upstream-ошибки
|
||||
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
|
||||
- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
|
||||
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
|
||||
|
||||
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio
|
||||
- **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 быть интерактивным или получать клавиатурный фокус.
|
||||
|
||||
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
|
||||
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
|
||||
30 секунд, без automatic retry или refetch при возврате фокуса.
|
||||
Polling допускается только для известной работы открытого экрана;
|
||||
data GET наблюдения использует `sync=observe` и MUST NOT запускать новый импорт.
|
||||
Status сообщает state/errorCode без hash notifications и численных progress counters.
|
||||
После ухода или завершения работы polling MUST прекращаться.
|
||||
Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями.
|
||||
|
||||
Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться.
|
||||
Новый диапазон свечей загружается при его открытии; initial loading допустим.
|
||||
При загрузке без ранее полученного успешного ответа для этого же Brokerage
|
||||
Account клиент MUST показывать структурное skeleton-состояние ожидаемого
|
||||
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
|
||||
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
|
||||
или получать клавиатурный фокус.
|
||||
|
||||
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
|
||||
Client Cached Snapshot. Во время фонового обновления и после его ошибки
|
||||
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
|
||||
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
|
||||
показывать компактную неблокирующую индикацию без замены данных skeleton.
|
||||
При ошибке обновления он MUST показывать локализованное неблокирующее
|
||||
предупреждение о последних полученных данных и предоставлять действие
|
||||
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
|
||||
снимком или подтверждением текущей доступности Brokerage Account.
|
||||
|
||||
Клиент MUST показать штатное empty-состояние только при пустых `positions`
|
||||
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
|
||||
Portfolio клиент MUST сохранять время успешного получения снимка
|
||||
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
|
||||
показывать Snapshot, его исходное время и нейтральное предупреждение
|
||||
о временной недоступности T-Bank. Это предупреждение само по себе 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-состояние с доступными
|
||||
для ошибки действиями; действие повтора разрешено только для временной
|
||||
и неизвестной ошибки.
|
||||
При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки.
|
||||
|
||||
#### Scenario: Показ устаревшего Portfolio
|
||||
|
||||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
|
||||
последнего успешного снимка и нейтральное предупреждение; само это
|
||||
предупреждение не добавляет действие ручного обновления
|
||||
|
||||
#### Scenario: Недоступный счёт на клиенте
|
||||
|
||||
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный
|
||||
Client Cached Snapshot и клиент получает `404`
|
||||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
|
||||
Brokerage Account без действия повтора
|
||||
- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации
|
||||
|
||||
#### Scenario: Первичная загрузка Portfolio
|
||||
- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа
|
||||
- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders
|
||||
|
||||
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
|
||||
полученного успешного ответа
|
||||
- **THEN** он показывает структурный skeleton, включая место сводки,
|
||||
с доступным обозначением загрузки, без вымышленных или нулевых сумм
|
||||
и без фокусируемых placeholders
|
||||
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
|
||||
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
|
||||
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки
|
||||
|
||||
#### Scenario: Недоступный счёт на клиенте
|
||||
- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404`
|
||||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
|
||||
|
||||
#### Scenario: Обновление после истечения срока свежести
|
||||
|
||||
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
|
||||
обновление данных того же Brokerage Account
|
||||
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
|
||||
получения, показывает неблокирующую индикацию обновления и не заменяет
|
||||
доступный состав и сводку skeleton-состоянием
|
||||
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание
|
||||
- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием
|
||||
|
||||
#### Scenario: Ошибка обновления после успешного ответа
|
||||
|
||||
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
|
||||
а обновление этих же данных завершается ошибкой
|
||||
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
|
||||
показывает неблокирующее предупреждение и действие повтора;
|
||||
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
|
||||
- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой
|
||||
- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения
|
||||
|
||||
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||
|
||||
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
|
||||
обновление этого же Portfolio завершается ошибкой
|
||||
- **THEN** прежние суммы и время остаются видимыми одновременно
|
||||
с нейтральным server-side предупреждением, отдельным предупреждением
|
||||
об ошибке клиентского обновления и его действием повтора
|
||||
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой
|
||||
- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению
|
||||
|
||||
#### Scenario: Повторное обновление успешно
|
||||
|
||||
- **WHEN** после ошибки фонового обновления User запускает повторную
|
||||
попытку и получает успешный Portfolio Snapshot
|
||||
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
|
||||
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
|
||||
убирает предупреждение об ошибке клиентского обновления и показывает
|
||||
server-side предупреждение только в соответствии с новым `isStale`
|
||||
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
|
||||
- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale`
|
||||
|
||||
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||
|
||||
- **WHEN** успешно полученный Portfolio содержит пустые `positions`
|
||||
и `cashBalances`
|
||||
- **THEN** клиент показывает существующее empty-состояние без валютных
|
||||
карточек с вымышленными нулевыми итогами
|
||||
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
|
||||
- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами
|
||||
|
||||
### Requirement: Представление карточек позиций на клиенте
|
||||
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции.
|
||||
|
||||
@@ -18,15 +18,15 @@
|
||||
- **THEN** система отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }` без token
|
||||
|
||||
### Requirement: Создание и замена подключения T-Bank
|
||||
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`.
|
||||
Система 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> }`
|
||||
- **THEN** система сохраняет подключение, отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }` и не создаёт фиктивные счета
|
||||
|
||||
#### Scenario: Успешная замена подключения
|
||||
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
|
||||
- **THEN** система заменяет token подключения, обновляет `connectedAt` и отвечает текущим статусом без token
|
||||
- **THEN** система заменяет token, обновляет `connectedAt`, отвечает текущим статусом без token и начинает первый часовой обход нового подключения
|
||||
|
||||
#### Scenario: Невалидный token не заменяет существующее подключение
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
|
||||
@@ -37,15 +37,15 @@
|
||||
- **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.
|
||||
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и сделать относящиеся к User текущие личные данные недоступными. История стоимости счёта MUST сохраняться, но MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
|
||||
|
||||
#### Scenario: Удаление существующего подключения
|
||||
- **WHEN** User удаляет существующее подключение
|
||||
- **THEN** система удаляет token и связанные кэшированные данные User, затем отвечает `204`
|
||||
- **THEN** система удаляет token, скрывает текущие личные данные и историю стоимости и отвечает `204`
|
||||
|
||||
#### Scenario: Повторное удаление подключения
|
||||
- **WHEN** User удаляет уже отсутствующее подключение
|
||||
- **THEN** система отвечает `204`
|
||||
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
|
||||
|
||||
### Requirement: Конфиденциальность token T-Bank
|
||||
Система MUST хранить не более одного token T-Bank для каждого User только в зашифрованном виде. Token MUST расшифровываться только для серверного вызова T-Bank и MUST NOT передаваться в frontend, HTTP-ответах, OpenAPI examples, сообщениях ошибок или application logs.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# tbank-data-sync Specification
|
||||
|
||||
## Purpose
|
||||
Определяет единственного владельца обновления личных данных T-Bank и узкие
|
||||
исключения первой загрузки данных, которых ещё нет в локальном результате.
|
||||
|
||||
## 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` обозначает доступный полный результат, его время и состояние последней часовой попытки
|
||||
Reference in New Issue
Block a user