Files
backend/src/modules/investments/portfolio/application/forecast.service.ts
T
ksv741 d698ed9425
Backend verify / commits (pull_request) Successful in 29s
Backend verify / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 9m36s
refactor!: simplify runtime coordination #7
Keep financial ownership, atomic publication and refresh CAS.
Failed refresh leaves cookies untouched; logout still clears them.

Remove full-history candle prefetch and special metadata retries.
Use native Pino request context instead of parallel middleware.

BREAKING CHANGE: remove investment stateVersion, queued sync state,
and tbank-sync status progress/resources. Consumers must observe
resources through sync=observe with the updated frontend contract.
2026-10-03 11:38:08 +03:00

94 lines
2.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import type { InvestmentResourceMetaDto } from '../../investment-resource.dto';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import {
ForecastRepository,
type ForecastSourceVersion,
} from '../infrastructure/forecast.repository';
const MISSING_DATA_REVISION = 0;
@Injectable()
export class ForecastService {
constructor(private readonly repository: ForecastRepository) {}
async result<T extends object>(input: {
candidate?: T;
membershipRevision: number;
portfolio: InvestmentResourceMetaDto;
revision: string;
scopeKey: string;
sources: {
meta: InvestmentResourceMetaDto;
version: ForecastSourceVersion | null;
}[];
userId: string;
}) {
const dependencies = [
input.portfolio,
...input.sources.map((source) => source.meta),
];
const stored = await this.repository.resolve({
...input,
portfolioProjection: input.portfolio.dataRevision,
portfolios: (input.portfolio.dependencies ?? []).flatMap((dependency) =>
dependency.resourceKey.startsWith('portfolio/')
? [
{
accountId: decodeURIComponent(
dependency.resourceKey.slice('portfolio/'.length),
),
dataRevision: Number(
(dependency as InvestmentResourceMetaDto).dataRevision,
),
},
]
: [],
),
sources: input.sources.flatMap((source) =>
source.version ? [source.version] : [],
),
timestamps: dependencies.flatMap((dependency) =>
dependency.lastSuccessfulAt
? [
{
resourceKey: dependency.resourceKey,
at: dependency.lastSuccessfulAt,
},
]
: [],
),
});
const timestamps = stored?.sourceTimestamps as
undefined | { at: string; resourceKey: string }[];
const first = timestamps?.map((entry) => entry.at).sort()[0];
const work =
dependencies
.map((dependency) => dependency.sync)
.find((sync) => sync.state === 'running') ??
dependencies
.map((dependency) => dependency.sync)
.find((sync) => sync.state === 'failed');
return {
data: stored ? (stored.payload as T) : null,
meta: investmentResourceMeta({
resourceKey: input.scopeKey,
dataRevision: String(stored?.dataRevision ?? MISSING_DATA_REVISION),
availability: stored
? 'complete'
: dependencies.some(
(dependency) => dependency.availability !== 'missing',
)
? 'partial'
: 'missing',
fetchedAt: first ? new Date(first) : null,
freshForMs: Number.POSITIVE_INFINITY,
work: work?.id ? { ...work, id: work.id } : null,
dependencies,
}),
};
}
}