fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s

Resolve A01-A10 and C01-C07 from the backend audit.
Preserve auth failures, forced candle reconciliation and forecast freshness.
Validate MOEX responses and nested HTTP errors; remove unused legacy paths.

Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability.
Record the remaining macOS native SQLite abort in the audit.
This commit is contained in:
2026-09-27 14:09:05 +03:00
parent 5d3835e796
commit 4796d9d3f0
55 changed files with 1954 additions and 569 deletions
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { tbankSourceNeedsRefresh } from '../../../../integrations/tbank/tbank.api';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import type { InvestmentResourceMetaDto } from '../../investment-resource.dto';
import {
@@ -67,8 +68,23 @@ export class ForecastService {
dependencies
.map((dependency) => dependency.sync)
.find((sync) => sync.state === 'failed');
const payload = stored ? (stored.payload as T) : null;
const data =
payload &&
'positionIsStale' in payload &&
'positionAt' in payload &&
typeof payload.positionAt === 'string'
? {
...payload,
positionIsStale: tbankSourceNeedsRefresh(
'portfolio',
new Date(payload.positionAt),
new Date(Date.now()),
),
}
: payload;
return {
data: stored ? (stored.payload as T) : null,
data,
meta: investmentResourceMeta({
resourceKey: input.scopeKey,
dataRevision: String(stored?.dataRevision ?? 0),
@@ -80,7 +96,10 @@ export class ForecastService {
? 'partial'
: 'missing',
fetchedAt: first ? new Date(first) : null,
freshForMs: Number.POSITIVE_INFINITY,
freshForMs:
data && 'positionIsStale' in data && data.positionIsStale === true
? 0
: Number.POSITIVE_INFINITY,
work: work?.id ? { ...work, id: work.id } : null,
dependencies,
}),
@@ -14,6 +14,7 @@ import {
} from '@nestjs/common';
import {
add,
decimalFromString,
formatDecimal,
isZero,
multiply,
@@ -511,12 +512,3 @@ export class PortfolioService {
: null;
}
}
function decimalFromString(value: string) {
const negative = value.startsWith('-');
const [whole, fraction = ''] = (negative ? value.slice(1) : value).split('.');
return {
value: (negative ? -1n : 1n) * BigInt(`${whole}${fraction}`),
scale: fraction.length,
};
}