[Bug] Публиковать новый полный forecast независимо от request window #55

Closed
opened 2026-10-11 21:41:56 +03:00 by codex-bot · 2 comments
Owner

Problem

resolve возвращает первый overlapping fallback до validation/upsert нового complete candidate. Daily window shift может удерживать прежние amounts. Disjoint saved-read возвращает null вместо empty filtered result. GET и scheduled producer делят publication; replacement после failed existing calendar пока runtime-unverified.

Expected Behavior

Новый согласованный полный forecast становится latest; фильтрующий GET сохраняет original calculatedAt/window/identity. Failed/partial update сохраняет previous complete result.

Evidence

Audit date: 2026-10-11. Reports: 06, 08. Finding IDs: BE-001, BE-013, ARCH-008, CPLX-004, FLOW-007, RISK-003, TQ-005, TA-F01.
Verified revision: 0ad84ff4f0b4712abc7e69dc299856f14af6eadd; Confidence: High (selection), Medium (failed-source GET policy). Verification status: static / runtime-unverified для BE-013.
Affected code: forecast.repository.ts:50–175; forecast.service.ts:77–123; upcoming-events.service.ts:152–173,271–299; test/upcoming-events.e2e-spec.ts:321–339,619–641.

Scope

In scope

Deterministic latest selection, producer/read distinction, account/all semantics, original-window wire artifact и все три ForecastService consumers.

Out of scope

Удаление history/old rows, ослабление dependency CAS, CQRS/new ForecastModule, guessed product policy.

Proposed Approach

Сначала controlled same-UID quantity change + failed existing calendar + normal GET: зафиксировать допустимую snapshot policy. Разделить selection/publication/filter у existing owners; additive transition только если меняется persisted key. Одного удаления early return недостаточно.

Acceptance Criteria

  • W0→W1 с изменёнными quantities/calendar публикует W1 при полном candidate, независимо от overlap.
  • Narrow/disjoint GET сохраняет original identity/time/window; disjoint data — successful empty, не fake missing.
  • Failed-existing-calendar policy явно решена и проверена; old full result не заменяется запрещённой mixed basis.
  • Connection/membership/portfolio/source race отвергает stale candidate, exact per-currency totals сохранены.
  • Account/all, stock dividends, bond coupons работают; published window artifact явно передан frontend без ручной generated TS правки.

Verification

Existing upcoming-events isolated SQLite/HTTP cases: shifted windows, disjoint/overlap, failure и dependency races. Existing narrow timestamp proof оставить. Runtime OpenAPI parity, frontend consumer check при artifact delivery; полный relevant verify на конечном diff.

Dependencies

Блокирующих Issues нет. Calendar force T04 использует принятую здесь failure/publication boundary.

Traceability

Campaign: WealthLedger/main#23 . Findings: BE-001, BE-013, ARCH-008, CPLX-004, FLOW-007, RISK-003, TQ-005, TA-F01. Repository: WealthLedger/backend; T02.

<!-- wealthledger-audit:2026-10-11:forecast-identity --> ## Problem resolve возвращает первый overlapping fallback до validation/upsert нового complete candidate. Daily window shift может удерживать прежние amounts. Disjoint saved-read возвращает null вместо empty filtered result. GET и scheduled producer делят publication; replacement после failed existing calendar пока runtime-unverified. ## Expected Behavior Новый согласованный полный forecast становится latest; фильтрующий GET сохраняет original calculatedAt/window/identity. Failed/partial update сохраняет previous complete result. ## Evidence Audit date: `2026-10-11`. Reports: [06](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/d07fbfabb1295780952a554185989d2d5b5c8349/audits/2026-10-11/reports/06-backend-architecture-report.md), [08](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/d07fbfabb1295780952a554185989d2d5b5c8349/audits/2026-10-11/reports/08-test-quality-and-effectiveness-report.md). Finding IDs: BE-001, BE-013, ARCH-008, CPLX-004, FLOW-007, RISK-003, TQ-005, TA-F01. Verified revision: `0ad84ff4f0b4712abc7e69dc299856f14af6eadd`; Confidence: High (selection), Medium (failed-source GET policy). Verification status: static / runtime-unverified для BE-013. Affected code: [forecast.repository.ts:50–175](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/0ad84ff4f0b4712abc7e69dc299856f14af6eadd/src/modules/investments/portfolio/infrastructure/forecast.repository.ts#L50-L175); forecast.service.ts:77–123; upcoming-events.service.ts:152–173,271–299; test/upcoming-events.e2e-spec.ts:321–339,619–641. ## Scope ### In scope Deterministic latest selection, producer/read distinction, account/all semantics, original-window wire artifact и все три ForecastService consumers. ### Out of scope Удаление history/old rows, ослабление dependency CAS, CQRS/new ForecastModule, guessed product policy. ## Proposed Approach Сначала controlled same-UID quantity change + failed existing calendar + normal GET: зафиксировать допустимую snapshot policy. Разделить selection/publication/filter у existing owners; additive transition только если меняется persisted key. Одного удаления early return недостаточно. ## Acceptance Criteria - [x] W0→W1 с изменёнными quantities/calendar публикует W1 при полном candidate, независимо от overlap. - [x] Narrow/disjoint GET сохраняет original identity/time/window; disjoint data — successful empty, не fake missing. - [x] Failed-existing-calendar policy явно решена и проверена; old full result не заменяется запрещённой mixed basis. - [x] Connection/membership/portfolio/source race отвергает stale candidate, exact per-currency totals сохранены. - [x] Account/all, stock dividends, bond coupons работают; published window artifact явно передан frontend без ручной generated TS правки. ## Verification Existing upcoming-events isolated SQLite/HTTP cases: shifted windows, disjoint/overlap, failure и dependency races. Existing narrow timestamp proof оставить. Runtime OpenAPI parity, frontend consumer check при artifact delivery; полный relevant verify на конечном diff. ## Dependencies Блокирующих Issues нет. Calendar force T04 использует принятую здесь failure/publication boundary. ## Traceability Campaign: https://git.personal.ksv741.ru/WealthLedger/main/issues/23 . Findings: BE-001, BE-013, ARCH-008, CPLX-004, FLOW-007, RISK-003, TQ-005, TA-F01. Repository: WealthLedger/backend; T02.
codex-bot added this to the Technical Audit Remediation — 2026-10-11 milestone 2026-10-11 21:41:56 +03:00
codex-bot added the Kind/Bug
Priority
High
2
audit/2026-10-11
labels 2026-10-11 21:41:56 +03:00
codex-bot was assigned by hermes-agent 2026-10-12 00:04:25 +03:00
hermes-agent added this to the WealthLedger — Delivery Board project 2026-10-12 00:04:25 +03:00
Author
Owner

Merged: PR #156 (squash, develop 169bee1). CI (commits, security, verify) was green before merge.

Decisions (owner, BE-013)

  • Failed/partial candidate for an existing holding: GET returns the previous complete forecast + meta (failed/partial, lastAttempt); the new candidate is not published, no mixed basis.
  • Only the default window publishes (hourly and GET without from/to). Explicit from/to reads and filters the latest forecast; it publishes only if nothing is saved yet.

Change

  • ForecastRepository.resolve: removed the early return of the first overlapping fallback before candidate validation/upsert. Latest = calculatedAt desc, scopeKey asc within account + current membership revision. Disjoint range returns a successful empty result with the original calculatedAt/window.
  • ForecastService: response carries the saved forecast's original window.
  • No schema/scopeKey change, no migration, old rows untouched. ForecastService consumers stock/dividends and bond/coupons use exact-key rows and were not modified.

Evidence

  • New/changed tests in test/upcoming-events.e2e-spec.ts (SQLite HTTP): 5 of 26 failed on the old code, 26/26 pass after the fix. Cases: W0→W1 with changed quantity (stale and disjoint explicit ranges serve W1), narrow explicit range does not replace the wide forecast, failed new-holding calendar after window shift keeps the previous forecast, shifted window published without dependencies, disjoint range = empty result instead of null.
  • npm run verify: lint, typecheck app+test, 42 suites / 310 tests, build — OK.
  • npm run test:e2e (all SQLite suites): exit 0.
  • npm run openapi:generate: schema contains UpcomingEventsDto.window (openapi.json is gitignored).

Not verified / limits

  • AC 4 (races, exact totals): covered by pre-existing cases (refuses a calculated snapshot if its portfolio changes…, discards a late calendar from a replaced connection…, per-currency totals) which still pass; no new race tests were added.
  • AC 5 is left unchecked: account/all, stock and bond paths are covered by pre-existing tests only (unchanged code). The frontend has not received/regenerated the artifact yet.
  • No runtime check against real T-Bank/MOEX (live calls forbidden), PostgreSQL or prod data.

API contract change (backend → frontend)

Additive: UpcomingEventsDto.window: { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } (required in the response, GET /brokerage-accounts/{id|all}/events). The current frontend is unaffected; types must be regenerated from the new OpenAPI artifact, no hand edits.

Merged: PR #156 (squash, develop `169bee1`). CI (commits, security, verify) was green before merge. ## Decisions (owner, BE-013) - Failed/partial candidate for an existing holding: GET returns the previous complete forecast + `meta` (failed/partial, `lastAttempt`); the new candidate is not published, no mixed basis. - Only the default window publishes (hourly and GET without `from`/`to`). Explicit `from`/`to` reads and filters the latest forecast; it publishes only if nothing is saved yet. ## Change - `ForecastRepository.resolve`: removed the early return of the first overlapping fallback before candidate validation/upsert. Latest = `calculatedAt desc, scopeKey asc` within account + current membership revision. Disjoint range returns a successful empty result with the original `calculatedAt`/window. - `ForecastService`: response carries the saved forecast's original window. - No schema/`scopeKey` change, no migration, old rows untouched. `ForecastService` consumers `stock/dividends` and `bond/coupons` use exact-key rows and were not modified. ## Evidence - New/changed tests in `test/upcoming-events.e2e-spec.ts` (SQLite HTTP): 5 of 26 failed on the old code, 26/26 pass after the fix. Cases: W0→W1 with changed quantity (stale and disjoint explicit ranges serve W1), narrow explicit range does not replace the wide forecast, failed new-holding calendar after window shift keeps the previous forecast, shifted window published without dependencies, disjoint range = empty result instead of `null`. - `npm run verify`: lint, typecheck app+test, 42 suites / 310 tests, build — OK. - `npm run test:e2e` (all SQLite suites): exit 0. - `npm run openapi:generate`: schema contains `UpcomingEventsDto.window` (`openapi.json` is gitignored). ## Not verified / limits - AC 4 (races, exact totals): covered by pre-existing cases (`refuses a calculated snapshot if its portfolio changes…`, `discards a late calendar from a replaced connection…`, per-currency totals) which still pass; no new race tests were added. - AC 5 is left unchecked: account/all, stock and bond paths are covered by pre-existing tests only (unchanged code). The frontend has not received/regenerated the artifact yet. - No runtime check against real T-Bank/MOEX (live calls forbidden), PostgreSQL or prod data. ## API contract change (backend → frontend) Additive: `UpcomingEventsDto.window: { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' }` (required in the response, `GET /brokerage-accounts/{id|all}/events`). The current frontend is unaffected; types must be regenerated from the new OpenAPI artifact, no hand edits.
Author
Owner

AC 5 closed; Issue closed. All five AC are checked.

Window artifact delivered to the frontend

  • WealthLedger/frontend#134, PR #135 (squash, frontend develop aaee959, CI green): src/shared/api/spec/openapi.json replaced by the backend OpenAPI document (only differences from the previous snapshot: UpcomingEventsDto.properties.window, its required entry, new schema UpcomingEventsWindowDto {from, to}; paths identical). schema.d.ts produced by npm run openapi:generate (+8 lines), not edited by hand. Frontend npm run verify (74 files / 553 tests, knip, depcruise, build, storybook build): exit 0.
  • Frontend code was not changed: window is not displayed yet. Showing it and the filters decision belong to WealthLedger/frontend#63.

Account/all, stock dividends, bond coupons

Covered by pre-existing SQLite HTTP cases in test/upcoming-events.e2e-spec.ts, which pass unchanged (suite 30/30, full npm run test:e2e exit 0): "preserves source identity, inclusive dates, exact currencies, cancellation and offers in account/all scopes", "serves instrument payouts from shared calendars with private exact holdings…" (stock dividends) and the stocks/bonds payout-position cases (bond coupons). The consumers StockIncomeService/BondIncomeService use exact-key rows and were not changed in #156. No new dedicated tests were added for them.

Not verified / limits

  • No runtime check against real T-Bank/MOEX (live calls forbidden), PostgreSQL or prod data; everything is SQLite + HTTP.
  • No new race tests (AC 4 relies on the pre-existing ones, which pass).

API contract

Additive UpcomingEventsDto.window: {from, to} (required in the response); frontend snapshot synchronized as above.

AC 5 closed; Issue closed. All five AC are checked. ## Window artifact delivered to the frontend - WealthLedger/frontend#134, PR #135 (squash, frontend develop `aaee959`, CI green): `src/shared/api/spec/openapi.json` replaced by the backend OpenAPI document (only differences from the previous snapshot: `UpcomingEventsDto.properties.window`, its `required` entry, new schema `UpcomingEventsWindowDto {from, to}`; paths identical). `schema.d.ts` produced by `npm run openapi:generate` (+8 lines), not edited by hand. Frontend `npm run verify` (74 files / 553 tests, knip, depcruise, build, storybook build): exit 0. - Frontend code was not changed: `window` is not displayed yet. Showing it and the filters decision belong to WealthLedger/frontend#63. ## Account/all, stock dividends, bond coupons Covered by pre-existing SQLite HTTP cases in `test/upcoming-events.e2e-spec.ts`, which pass unchanged (suite 30/30, full `npm run test:e2e` exit 0): "preserves source identity, inclusive dates, exact currencies, cancellation and offers in account/all scopes", "serves instrument payouts from shared calendars with private exact holdings…" (stock dividends) and the `stocks`/`bonds` payout-position cases (bond coupons). The consumers `StockIncomeService`/`BondIncomeService` use exact-key rows and were not changed in #156. No new dedicated tests were added for them. ## Not verified / limits - No runtime check against real T-Bank/MOEX (live calls forbidden), PostgreSQL or prod data; everything is SQLite + HTTP. - No new race tests (AC 4 relies on the pre-existing ones, which pass). ## API contract Additive `UpcomingEventsDto.window: {from, to}` (required in the response); frontend snapshot synchronized as above.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: WealthLedger/backend#55