spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s

This commit is contained in:
2026-10-09 20:41:46 +03:00
parent b24a686188
commit 13e3e53d06
97 changed files with 3444 additions and 244 deletions
@@ -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.
@@ -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.
@@ -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
@@ -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.
@@ -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** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной
@@ -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-чтение
@@ -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
@@ -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` и его исходное время видимыми и отдельно обозначает неуспех попытки
@@ -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** система не раскрывает результат прежней границы подключения
@@ -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** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных
@@ -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** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений
@@ -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-состояние без валютных карточек с вымышленными нулевыми итогами
@@ -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`, не раскрывая сохранённую историю
@@ -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` обозначает доступный полный результат, его время и состояние последней часовой попытки
@@ -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.
@@ -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"] }
}
}
}
}
@@ -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
@@ -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