chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 11:04:18 +03:00
parent ca5dbcb382
commit 9b3558f96c
179 changed files with 4485 additions and 2716 deletions
@@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common';
import type { InvestmentResourceMetaDto } from '../../investment-resource.dto';
import { tbankSourceNeedsRefresh } from '../../../../integrations/tbank/tbank.api';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import type { InvestmentResourceMetaDto } from '../../investment-resource.dto';
import {
ForecastRepository,
type ForecastSourceVersion,
@@ -12,16 +14,16 @@ export class ForecastService {
constructor(private readonly repository: ForecastRepository) {}
async result<T extends object>(input: {
userId: string;
revision: string;
membershipRevision: number;
scopeKey: string;
portfolio: InvestmentResourceMetaDto;
sources: {
version: ForecastSourceVersion | null;
meta: InvestmentResourceMetaDto;
}[];
candidate?: T;
membershipRevision: number;
portfolio: InvestmentResourceMetaDto;
revision: string;
scopeKey: string;
sources: {
meta: InvestmentResourceMetaDto;
version: ForecastSourceVersion | null;
}[];
userId: string;
}) {
const dependencies = [
input.portfolio,
@@ -31,8 +33,8 @@ export class ForecastService {
...input,
portfolioProjection: input.portfolio.dataRevision,
portfolios: (input.portfolio.dependencies ?? []).flatMap((dependency) =>
dependency.resourceKey.startsWith('portfolio/')
? [
dependency.resourceKey.startsWith('portfolio/') ?
[
{
accountId: decodeURIComponent(
dependency.resourceKey.slice('portfolio/'.length),
@@ -41,65 +43,65 @@ export class ForecastService {
(dependency as InvestmentResourceMetaDto).dataRevision,
),
},
]
: [],
] :
[],
),
sources: input.sources.flatMap((source) =>
source.version ? [source.version] : [],
),
timestamps: dependencies.flatMap((dependency) =>
dependency.lastSuccessfulAt
? [
dependency.lastSuccessfulAt ?
[
{
resourceKey: dependency.resourceKey,
at: dependency.lastSuccessfulAt,
},
]
: [],
] :
[],
),
});
const timestamps = stored?.sourceTimestamps as
{ resourceKey: string; at: string }[] | undefined;
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');
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'
? {
typeof payload.positionAt === 'string' ?
{
...payload,
positionIsStale: tbankSourceNeedsRefresh(
'portfolio',
new Date(payload.positionAt),
new Date(Date.now()),
),
}
: payload;
} :
payload;
return {
data,
meta: investmentResourceMeta({
resourceKey: input.scopeKey,
dataRevision: String(stored?.dataRevision ?? 0),
availability: stored
? 'complete'
: dependencies.some(
(dependency) => dependency.availability !== 'missing',
)
? 'partial'
: 'missing',
availability: stored ?
'complete' :
dependencies.some(
(dependency) => dependency.availability !== 'missing',
) ?
'partial' :
'missing',
fetchedAt: first ? new Date(first) : null,
freshForMs:
data && 'positionIsStale' in data && data.positionIsStale === true
? 0
: Number.POSITIVE_INFINITY,
data && 'positionIsStale' in data && data.positionIsStale === true ?
0 :
Number.POSITIVE_INFINITY,
work: work?.id ? { ...work, id: work.id } : null,
dependencies,
}),
@@ -1,10 +1,10 @@
import { setImmediate as immediate } from 'node:timers/promises';
import { ServiceUnavailableException } from '@nestjs/common';
import { setImmediate as immediate } from 'node:timers/promises';
jest.mock('../../../../prisma/prisma.service', () => ({
PrismaService: class {},
}));
import { PortfolioService } from './portfolio.service';
import { PortfolioDto } from '../presentation/portfolio.dto';
import { PortfolioService } from './portfolio.service';
describe('persistent portfolio projection', () => {
const access = { local: jest.fn(), tokenForAttempt: jest.fn() };
@@ -103,8 +103,9 @@ describe('persistent portfolio projection', () => {
let index = 0;
index < 10 && !repository.publish.mock.calls.length;
index += 1
)
) {
await immediate();
}
const snapshot = (
repository.publish.mock.calls as [unknown, string, PortfolioDto][]
)[0][2];
@@ -1,17 +1,10 @@
import { CandleService } from '../../instruments/instruments.api';
import { randomUUID } from 'node:crypto';
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import {
PortfolioRepository,
portfolioFreshForMs,
portfolioWorkKey,
} from '../infrastructure/portfolio.repository';
import {
Injectable,
ServiceUnavailableException,
NotFoundException,
ServiceUnavailableException,
} from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import {
add,
decimalFromString,
@@ -21,22 +14,30 @@ import {
subtract,
} from '../../../../common/decimal';
import { RequestContextService } from '../../../../common/logging/request-context.service';
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
import { MoneyDto } from '../../../../common/money.dto';
import { decimal } from '../../../../integrations/tbank/decimal';
import {
InstrumentMetadataService,
metadataKey,
type TbankInstrument,
type MetadataIdentity,
metadataKey,
TbankAccessService,
TbankClient,
TbankMoney,
type TbankInstrument,
TbankMembershipService,
TbankMoney,
tbankSourceTtlMs,
} from '../../../../integrations/tbank/tbank.api';
import { decimal } from '../../../../integrations/tbank/decimal';
import { CandleService } from '../../instruments/instruments.api';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import { portfolioStructure } from '../domain/portfolio-structure';
import { totalsByCurrency } from '../infrastructure/tbank-portfolio.mapper';
import { sumMoneyByCurrency } from '../domain/portfolio-totals';
import {
portfolioFreshForMs,
PortfolioRepository,
portfolioWorkKey,
} from '../infrastructure/portfolio.repository';
import { totalsByCurrency } from '../infrastructure/tbank-portfolio.mapper';
import {
PortfolioDto,
PortfolioPositionDto,
@@ -80,7 +81,7 @@ export class PortfolioService {
userId,
connection.revision,
);
if (discovery)
if (discovery) {
void discovery.done
.then(async () => {
const current = await this.access.local(userId, accountId);
@@ -88,12 +89,13 @@ export class PortfolioService {
for (const account of current.accounts.filter(
(account) =>
accountId === 'all' || account.accountId === accountId,
))
)) {
await this.queueSnapshot(
userId,
current.revision,
account.accountId,
);
}
})
.catch(() =>
this.logger.event(
@@ -102,22 +104,25 @@ export class PortfolioService {
{ userId },
),
);
}
for (const account of (discovery ? [] : connection.accounts).filter(
(account) => accountId === 'all' || account.accountId === accountId,
))
)) {
await this.queueSnapshot(
userId,
connection.revision,
account.accountId,
);
}
}
const saved = await this.repository.read(userId, connection.revision);
if (
saved?.membershipAt &&
accountId !== 'all' &&
!saved.accounts.some((account) => account.accountId === accountId)
)
) {
throw new NotFoundException();
}
if (!observe) {
const identities = new Map<string, MetadataIdentity>();
for (const account of (saved?.accounts ?? []).filter(
@@ -125,17 +130,20 @@ export class PortfolioService {
)) {
const snapshot = account.portfolio?.payload as unknown as
PortfolioDto | undefined;
for (const position of snapshot?.positions ?? [])
for (const position of snapshot?.positions ?? []) {
identities.set(metadataKey(position), position);
}
}
const savedMetadata = await this.metadata.savedMany([
...identities.values(),
]);
for (const identity of identities.values())
if (!savedMetadata.get(metadataKey(identity))?.fresh)
for (const identity of identities.values()) {
if (!savedMetadata.get(metadataKey(identity))?.fresh) {
await this.metadata.request(userId, connection.revision, identity);
}
}
}
if (!observe)
if (!observe) {
void this.candles
.prefetch(
userId,
@@ -156,6 +164,7 @@ export class PortfolioService {
.catch(() =>
this.logger.event('warn', 'portfolio.candle-prefetch-unavailable'),
);
}
const membershipMeta = investmentResourceMeta({
resourceKey: 'accounts/membership',
dataRevision: String(saved?.membershipRevision ?? 0),
@@ -175,15 +184,15 @@ export class PortfolioService {
name: account.name,
type: account.type,
openedOn: account.openedOn,
snapshot: account.portfolio
? {
snapshot: account.portfolio ?
{
...(account.portfolio.payload as unknown as PortfolioDto),
lastSuccessfulAt: account.portfolio.fetchedAt.toISOString(),
isStale:
Date.now() - account.portfolio.fetchedAt.valueOf() >=
portfolioFreshForMs,
}
: null,
} :
null,
meta: investmentResourceMeta({
resourceKey: `portfolio/${encodeURIComponent(account.accountId)}`,
dataRevision: String(account.portfolio?.dataRevision ?? 0),
@@ -209,8 +218,8 @@ export class PortfolioService {
);
const metadataDependencies = [...metadata.entries()].flatMap(
([key, source]) =>
source.complete && source.data
? [
source.complete && source.data ?
[
investmentResourceMeta({
resourceKey: `instrument/metadata/${key}`,
dataRevision: source.dataRevision,
@@ -219,20 +228,20 @@ export class PortfolioService {
freshForMs: tbankSourceTtlMs.metadata,
work: null,
}),
]
: [],
] :
[],
);
const portfolios = saved.accounts.flatMap((account) =>
account.snapshot
? [
account.snapshot ?
[
{
...account.snapshot,
positions: account.snapshot.positions.map((position) => {
const shared = metadata.get(metadataKey(position))?.data;
return {
...position,
...(shared
? {
...(shared ?
{
displayName:
shared.name?.trim() ||
shared.ticker?.trim() ||
@@ -241,28 +250,29 @@ export class PortfolioService {
isin: shared.isin?.trim() || position.isin,
logoName:
shared.brand?.logoName?.trim() || position.logoName,
}
: {}),
...(accountId === 'all'
? { account: { id: account.id, name: account.name } }
: {}),
} :
{}),
...(accountId === 'all' ?
{ account: { id: account.id, name: account.name } } :
{}),
};
}),
},
]
: [],
] :
[],
);
const lastSuccessfulAt =
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
portfolios.map((portfolio) => portfolio.lastSuccessfulAt).sort()[0] ??
saved.membership.lastSuccessfulAt;
const data =
missing || !lastSuccessfulAt
? null
: {
missing || !lastSuccessfulAt ?
null :
{
lastSuccessfulAt,
isStale: portfolios.length
? portfolios.some((portfolio) => portfolio.isStale)
: saved.membership.freshness !== 'fresh',
isStale: portfolios.length ?
portfolios.some((portfolio) => portfolio.isStale) :
saved.membership.freshness !== 'fresh',
totalsByCurrency: sumMoneyByCurrency(
portfolios.flatMap((portfolio) => portfolio.totalsByCurrency),
),
@@ -279,10 +289,10 @@ export class PortfolioService {
saved.accounts
.map((account) => account.meta.sync)
.find((work) => work.state === 'running') ??
saved.accounts
.map((account) => account.meta.sync)
.find((work) => work.state === 'failed') ??
saved.membership.sync;
saved.accounts
.map((account) => account.meta.sync)
.find((work) => work.state === 'failed') ??
saved.membership.sync;
return {
data,
meta: investmentResourceMeta({
@@ -294,15 +304,15 @@ export class PortfolioService {
]),
metadataDependencies.map((dependency) => dependency.dataRevision),
]),
availability: missing
? portfolios.length
? 'partial'
: 'missing'
: 'complete',
availability: missing ?
portfolios.length ?
'partial' :
'missing' :
'complete',
fetchedAt: lastSuccessfulAt ? new Date(lastSuccessfulAt) : null,
freshForMs: portfolios.length
? portfolioFreshForMs
: membershipFreshForMs,
freshForMs: portfolios.length ?
portfolioFreshForMs :
membershipFreshForMs,
work: work.id ? { ...work, id: work.id } : null,
dependencies: [
saved.membership,
@@ -333,11 +343,12 @@ export class PortfolioService {
force,
);
if (!requested) return null;
if (!requested.launch)
if (!requested.launch) {
return {
id: requested.work.id,
done: this.pending.get(requested.work.attemptId) ?? Promise.resolve(),
};
}
const { id, attemptId } = requested.work;
this.active.add(attemptId);
const done = new Promise<void>((resolve) => setImmediate(resolve))
@@ -360,22 +371,24 @@ export class PortfolioService {
snapshot,
);
if (published) {
for (const position of snapshot.positions)
for (const position of snapshot.positions) {
await this.metadata.request(userId, revision, position);
if (!force)
}
if (!force) {
await this.candles.prefetch(
userId,
revision,
snapshot.positions,
);
}
}
} catch (error) {
if (
error instanceof Error &&
error.message === 'INVALID_TBANK_TOKEN'
)
) {
await this.access.rejectMembershipToken(attempt);
else await this.repository.fail(attempt);
} else await this.repository.fail(attempt);
}
})
.catch(() =>
@@ -396,8 +409,9 @@ export class PortfolioService {
async get(userId: string, accountId: string): Promise<PortfolioDto> {
const result = await this.read(userId, accountId);
if (!result.data)
if (!result.data) {
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
return result.data;
}
@@ -407,12 +421,12 @@ export class PortfolioService {
private async normalize(
upstream: {
figi?: string;
instrumentUid?: string;
instrumentType?: string;
quantity?: { units?: string | number; nano?: string | number };
currentPrice?: TbankMoney;
averagePositionPrice?: TbankMoney;
currentPrice?: TbankMoney;
figi?: string;
instrumentType?: string;
instrumentUid?: string;
quantity?: { nano?: number | string; units?: number | string };
}[],
money: TbankMoney[],
): Promise<PortfolioDto> {
@@ -450,8 +464,8 @@ export class PortfolioService {
private position(
position: Parameters<PortfolioService['normalize']>[0][number],
metadata: TbankInstrument | null,
): PortfolioPositionDto | null {
metadata: null | TbankInstrument,
): null | PortfolioPositionDto {
const quantity = decimal(position.quantity ?? {});
if (isZero(quantity)) return null;
const figi = position.figi?.trim() || null;
@@ -462,8 +476,8 @@ export class PortfolioService {
const absoluteReturn =
currentPrice &&
averagePurchasePrice &&
currentPrice.currency === averagePurchasePrice.currency
? {
currentPrice.currency === averagePurchasePrice.currency ?
{
currency: currentPrice.currency,
value: formatDecimal(
multiply(
@@ -474,8 +488,8 @@ export class PortfolioService {
quantity,
),
),
}
: null;
} :
null;
return {
figi,
instrumentUid,
@@ -507,8 +521,8 @@ export class PortfolioService {
private money(value: TbankMoney | undefined): MoneyDto | null {
const currency = value?.currency?.trim().toUpperCase();
return currency && value
? { currency, value: formatDecimal(decimal(value)) }
: null;
return currency && value ?
{ currency, value: formatDecimal(decimal(value)) } :
null;
}
}
@@ -1,9 +1,9 @@
import { portfolioStructure } from './portfolio-structure';
import { PortfolioPositionDto } from '../presentation/portfolio.dto';
import { portfolioStructure } from './portfolio-structure';
const position = (
type: string,
value: string | null,
value: null | string,
currency = 'RUB',
): PortfolioPositionDto => ({
type,
@@ -1,11 +1,11 @@
import type { Money } from '../../../../common/money.types';
import {
add,
decimalFromString,
formatDecimal,
} from '../../../../common/decimal';
import type { Money } from '../../../../common/money.types';
const SHARE_PERCENT_DECIMAL_PLACES = 6;
const PERCENT_SCALED_FACTOR = 100_000_000n;
@@ -13,14 +13,14 @@ const categoryTypes = ['STOCK', 'BOND', 'OTHER', 'CASH'] as const;
// Build from positions and cash, never from totals (which already include cash).
export function portfolioStructure(
positions: { type: string; currentValue: Money | null }[],
positions: { currentValue: Money | null; type: string }[],
cash: Money[],
) {
const currencies = new Map<
string,
{
values: ReturnType<typeof decimalFromString>[];
hasNegativeValues: boolean;
values: ReturnType<typeof decimalFromString>[];
}
>();
const unvalued = new Map<string, number>();
@@ -31,14 +31,15 @@ export function portfolioStructure(
};
const index = categoryTypes.findIndex((category) => category === type);
const value = decimalFromString(money.value);
// eslint-disable-next-line security/detect-object-injection -- The numeric index belongs to the fixed portfolio category array.
group.values[index] = add(group.values[index], value);
group.hasNegativeValues ||= value.value < 0n;
currencies.set(money.currency, group);
};
for (const position of positions) {
const type = ['STOCK', 'BOND'].includes(position.type)
? position.type
: 'OTHER';
const type = ['BOND', 'STOCK'].includes(position.type) ?
position.type :
'OTHER';
if (position.currentValue) include(type, position.currentValue);
else unvalued.set(type, (unvalued.get(type) ?? 0) + 1);
}
@@ -53,21 +54,22 @@ export function portfolioStructure(
const canCalculateShares =
!unvalued.size && !group.hasNegativeValues && total.value > 0n;
const categories = group.values.map((value, index) => ({
// eslint-disable-next-line security/detect-object-injection -- The numeric index belongs to the fixed portfolio category array.
type: categoryTypes[index],
value: formatDecimal(value),
// Rounded to six decimal places using integer arithmetic. Only the
// resulting dimensionless percentage is passed to chart geometry.
sharePercent: canCalculateShares
? formatDecimal({
sharePercent: canCalculateShares ?
formatDecimal({
value:
(value.value *
10n ** BigInt(total.scale - value.scale) *
PERCENT_SCALED_FACTOR +
total.value / 2n) /
total.value,
total.value,
scale: SHARE_PERCENT_DECIMAL_PLACES,
})
: null,
}) :
null,
}));
return {
currency,
@@ -1,9 +1,10 @@
import type { Money } from '../../../../common/money.types';
import {
add,
decimalFromString,
formatDecimal,
} from '../../../../common/decimal';
import type { Money } from '../../../../common/money.types';
export function sumMoneyByCurrency(amounts: Money[]): Money[] {
const totals = new Map<string, ReturnType<typeof decimalFromString>>();
@@ -1,15 +1,17 @@
import { Injectable } from '@nestjs/common';
import { isDeepStrictEqual } from 'node:util';
import type { Prisma } from '../../../../../generated/prisma/client';
import { PrismaService } from '../../../../prisma/prisma.service';
export type ForecastSourceVersion = {
instrumentUid: string;
source: string;
kind: string;
rangeKey: string;
dataRevision: number;
fetchedAt: Date;
instrumentUid: string;
kind: string;
rangeKey: string;
source: string;
};
const forecastSourceReadBatchSize = 100;
@@ -18,15 +20,15 @@ export class ForecastRepository {
constructor(private readonly prisma: PrismaService) {}
resolve(input: {
userId: string;
revision: string;
membershipRevision: number;
scopeKey: string;
candidate?: object;
membershipRevision: number;
portfolioProjection: string;
portfolios: { accountId: string; dataRevision: number }[];
revision: string;
scopeKey: string;
sources: ForecastSourceVersion[];
timestamps: { resourceKey: string; at: string }[];
timestamps: { at: string; resourceKey: string }[];
userId: string;
}) {
return this.prisma.$transaction(async (tx) => {
const owner = { userId: input.userId, revision: input.revision };
@@ -54,9 +56,9 @@ export class ForecastRepository {
where: { userId_revision_scopeKey: key },
});
const fallback =
previous?.membershipRevision === input.membershipRevision
? previous
: null;
previous?.membershipRevision === input.membershipRevision ?
previous :
null;
if (!input.candidate) return fallback;
const portfolios = await tx.tbankPortfolio.findMany({
where: {
@@ -74,15 +76,16 @@ export class ForecastRepository {
row.dataRevision === expected.dataRevision,
),
)
)
) {
return fallback;
}
// Four identity parameters per source: stay below SQLite's parameter limit.
const sources: ForecastSourceVersion[] = [];
for (
let offset = 0;
offset < input.sources.length;
offset += forecastSourceReadBatchSize
)
) {
sources.push(
...(await tx.tbankInstrumentSource.findMany({
where: {
@@ -106,6 +109,7 @@ export class ForecastRepository {
},
})),
);
}
if (
input.sources.some(
(expected) =>
@@ -119,8 +123,9 @@ export class ForecastRepository {
row.fetchedAt.valueOf() === expected.fetchedAt.valueOf(),
),
)
)
) {
return fallback;
}
const revisions = JSON.parse(
JSON.stringify({
portfolioProjection: input.portfolioProjection,
@@ -133,8 +138,9 @@ export class ForecastRepository {
fallback &&
isDeepStrictEqual(fallback.dependencyRevisions, revisions) &&
isDeepStrictEqual(fallback.sourceTimestamps, timestamps)
)
) {
return fallback;
}
const data = {
membershipRevision: input.membershipRevision,
payload: JSON.parse(
@@ -1,18 +1,20 @@
import {
tbankSourceTtlMs,
tbankSourceNeedsRefresh,
} from '../../../../integrations/tbank/tbank.api';
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { PrismaService } from '../../../../prisma/prisma.service';
import type { Prisma } from '../../../../../generated/prisma/client';
import type { PortfolioDto } from '../presentation/portfolio.dto';
import {
tbankSourceNeedsRefresh,
tbankSourceTtlMs,
} from '../../../../integrations/tbank/tbank.api';
import { PrismaService } from '../../../../prisma/prisma.service';
type PortfolioAttempt = {
id: string;
attemptId: string;
userId: string;
id: string;
revision: string;
userId: string;
};
export const portfolioFreshForMs = tbankSourceTtlMs.portfolio;
@@ -71,8 +73,9 @@ export class PortfolioRepository {
!force &&
snapshot &&
!tbankSourceNeedsRefresh('portfolio', snapshot.fetchedAt, new Date())
)
) {
return null;
}
const scope = { userId, revision, scopeKey: portfolioWorkKey(accountId) };
const previous = await tx.tbankSyncWork.findUnique({
where: { userId_revision_scopeKey: scope },
@@ -81,8 +84,9 @@ export class PortfolioRepository {
previous?.state === 'running' &&
previous.processRunId === processRunId &&
active.has(previous.attemptId)
)
) {
return { work: previous, launch: false };
}
const data = {
state: 'running',
attemptId: randomUUID(),
@@ -1,12 +1,12 @@
import type { Money } from '../../../../common/money.types';
import {
add,
formatDecimal,
isZero,
multiply,
} from '../../../../common/decimal';
import type { Money } from '../../../../common/money.types';
import { decimal } from '../../../../integrations/tbank/decimal';
import {
TbankPortfolio,
TbankPositions,
@@ -1,8 +1,8 @@
export { ForecastService } from './application/forecast.service';
export { PortfolioService } from './application/portfolio.service';
// Public cross-module surface. Nest providers are exported by the matching module.
export {
PortfolioDto,
PortfolioPositionDto,
} from './presentation/portfolio.dto';
export { PortfolioService } from './application/portfolio.service';
export { ForecastService } from './application/forecast.service';
@@ -1,12 +1,13 @@
import { ForecastService } from './application/forecast.service';
import { ForecastRepository } from './infrastructure/forecast.repository';
import { InstrumentSourcesModule } from '../instruments/instrument-sources.module';
import { PortfolioRepository } from './infrastructure/portfolio.repository';
import { Module } from '@nestjs/common';
import { TbankModule } from '../../../integrations/tbank/tbank.module';
import { AuthModule } from '../../auth/auth.module';
import { PortfolioController } from './presentation/portfolio.controller';
import { InstrumentSourcesModule } from '../instruments/instrument-sources.module';
import { ForecastService } from './application/forecast.service';
import { PortfolioService } from './application/portfolio.service';
import { ForecastRepository } from './infrastructure/forecast.repository';
import { PortfolioRepository } from './infrastructure/portfolio.repository';
import { PortfolioController } from './presentation/portfolio.controller';
@Module({
imports: [AuthModule, TbankModule, InstrumentSourcesModule],
@@ -1,11 +1,13 @@
import { InvestmentReadQueryDto } from '../../investment-resource.dto';
import { Controller, Get, Param, Req, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import type { Request } from 'express';
import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ApiProblemResponses } from '../../../../common/decorators/api-safe-error-response.decorator';
import { AccessPayload, JwtAuthGuard } from '../../../auth/auth.api';
import { PortfolioResourceDto } from './portfolio.dto';
import { InvestmentReadQueryDto } from '../../investment-resource.dto';
import { PortfolioService } from '../application/portfolio.service';
import { PortfolioResourceDto } from './portfolio.dto';
@ApiTags('brokerage-accounts')
@ApiProblemResponses()
@@ -1,17 +1,18 @@
import { InvestmentResourceMetaDto } from '../../investment-resource.dto';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { MoneyDto } from '../../../../common/money.dto';
import { AccountReferenceDto } from '../../account-reference.dto';
import { InvestmentResourceMetaDto } from '../../investment-resource.dto';
export class PortfolioPositionDto {
@ApiPropertyOptional({ type: String, nullable: true })
instrumentUid?: string | null;
instrumentUid?: null | string;
@ApiPropertyOptional({ type: AccountReferenceDto })
account?: AccountReferenceDto;
@ApiProperty({ type: String, nullable: true })
declare figi: string | null;
declare figi: null | string;
@ApiProperty()
declare displayName: string;
@@ -44,13 +45,13 @@ export class PortfolioPositionDto {
declare absoluteReturn: MoneyDto | null;
@ApiProperty({ type: String, nullable: true })
declare isin: string | null;
declare isin: null | string;
@ApiProperty({ type: String, nullable: true })
declare ticker: string | null;
declare ticker: null | string;
@ApiProperty({ type: String, nullable: true })
declare logoName: string | null;
declare logoName: null | string;
}
export class PortfolioStructureCategoryDto {
@@ -66,7 +67,7 @@ export class PortfolioStructureCategoryDto {
description:
'Percentage including cash, rounded to six decimal places. Null for incomplete, negative or zero-total compositions.',
})
declare sharePercent: string | null;
declare sharePercent: null | string;
}
export class PortfolioStructureCurrencyDto {
@@ -81,7 +82,7 @@ export class PortfolioStructureCurrencyDto {
})
declare hasNegativeValues: boolean;
@ApiProperty({ type: [PortfolioStructureCategoryDto] })
@ApiProperty({ type: PortfolioStructureCategoryDto, isArray: true })
declare categories: PortfolioStructureCategoryDto[];
}
@@ -94,11 +95,11 @@ export class PortfolioUnvaluedCategoryDto {
}
export class PortfolioStructureDto {
@ApiProperty({ type: [PortfolioStructureCurrencyDto] })
@ApiProperty({ type: PortfolioStructureCurrencyDto, isArray: true })
declare currencies: PortfolioStructureCurrencyDto[];
@ApiProperty({
type: [PortfolioUnvaluedCategoryDto],
type: PortfolioUnvaluedCategoryDto, isArray: true,
description:
'Positions without current value and known valuation currency. Shares are unavailable across all currencies when nonempty.',
})
@@ -127,7 +128,8 @@ export class PortfolioDto {
export class PortfolioResourceDto {
@ApiProperty({ type: PortfolioDto, nullable: true })
declare data: PortfolioDto | null;
declare data: null | PortfolioDto;
@ApiProperty({ type: InvestmentResourceMetaDto })
declare meta: InvestmentResourceMetaDto;
}