chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 16:37:01 +03:00
committed by ksv741
parent 53b2794963
commit a970eaaf7a
179 changed files with 4485 additions and 2716 deletions
@@ -40,8 +40,9 @@ describe('manual sync screen boundary', () => {
from: '2026-02-01',
to: '2026-01-01',
},
] as Parameters<typeof normalizeScreen>[0][])
] as Parameters<typeof normalizeScreen>[0][]) {
expect(() => normalizeScreen(scope)).toThrow();
}
});
it('retains inclusive calendar days, rejecting instant values for date-only owners', () => {
const scope = {
@@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import type { InvestmentScreenDto } from './tbank-sync.dto';
// Reject fields from other screens before reserving any source work.
@@ -41,68 +42,78 @@ export function normalizeScreen(
});
};
if (!allowed[input.screen]) fail();
for (const key of Object.keys(input))
for (const key of Object.keys(input)) {
if (
key !== 'screen' &&
input[key as keyof InvestmentScreenDto] !== undefined &&
!allowed[input.screen].includes(key)
)
) {
fail();
}
}
const accountId = input.accountId?.trim();
const query = input.query?.trim().toUpperCase();
if (
['account', 'operations', 'income', 'events'].includes(input.screen) &&
['account', 'events', 'income', 'operations'].includes(input.screen) &&
!accountId
)
) {
fail();
if (
['stocks', 'bonds'].includes(input.screen) &&
(!query || query.length < 2)
)
fail();
if (['stock', 'bond'].includes(input.screen) && !input.instrumentUid) fail();
if (input.accountId !== undefined && !accountId) fail();
if (['income', 'events'].includes(input.screen)) {
if (input.screen === 'income' && Boolean(input.from) !== Boolean(input.to))
fail();
for (const value of [input.from, input.to])
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) fail();
}
if (input.from && input.to && new Date(input.from) > new Date(input.to))
if (
['bonds', 'stocks'].includes(input.screen) &&
(!query || query.length < 2)
) {
fail();
}
if (['bond', 'stock'].includes(input.screen) && !input.instrumentUid) fail();
if (input.accountId !== undefined && !accountId) fail();
if (['events', 'income'].includes(input.screen)) {
if (input.screen === 'income' && Boolean(input.from) !== Boolean(input.to)) {
fail();
}
for (const value of [input.from, input.to]) {
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) fail();
}
}
if (input.from && input.to && new Date(input.from) > new Date(input.to)) {
fail();
}
if (
input.screen === 'events' &&
input.to &&
(input.from ?? new Date().toISOString().slice(0, 10)) > input.to
)
) {
fail();
}
if (
input.candleRange &&
new Date(input.candleRange.from) >= new Date(input.candleRange.to)
)
) {
fail();
}
// Fixed field order is part of coalescing, irrespective of JSON input order.
return {
screen: input.screen,
...(accountId ? { accountId } : {}),
...(input.from ? { from: input.from } : {}),
...(input.to ? { to: input.to } : {}),
...(input.instrumentUid
? { instrumentUid: input.instrumentUid.toLowerCase() }
: {}),
...(input.instrumentUid ?
{ instrumentUid: input.instrumentUid.toLowerCase() } :
{}),
...(query ? { query } : {}),
...(input.candlePeriod ? { candlePeriod: input.candlePeriod } : {}),
...(input.candleRange
? {
...(input.candleRange ?
{
candleRange: {
from: new Date(input.candleRange.from).toISOString(),
to: new Date(input.candleRange.to).toISOString(),
},
}
: {}),
...(input.categories
? { categories: [...new Set(input.categories)].sort() }
: {}),
} :
{}),
...(input.categories ?
// 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.
{ categories: [...new Set(input.categories)].sort() } :
{}),
...(input.limit !== undefined ? { limit: input.limit } : {}),
};
}
@@ -1,6 +1,8 @@
import type { Request } from 'express';
import {
Body,
BadRequestException,
Body,
Controller,
Get,
HttpCode,
@@ -19,17 +21,17 @@ import {
ApiOkResponse,
ApiTags,
} from '@nestjs/swagger';
import type { Request } from 'express';
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
import { AccessPayload, JwtAuthGuard } from '../../auth/auth.api';
import {
InvestmentScreenDto,
IncomeInvestmentScreenDto,
EventsInvestmentScreenDto,
TimestampInvestmentScreenDto,
IncomeInvestmentScreenDto,
InvestmentScreenDto,
investmentScreenSchema,
TbankSyncAcceptedDto,
TbankSyncStatusDto,
TimestampInvestmentScreenDto,
} from './tbank-sync.dto';
import { TbankSyncService } from './tbank-sync.service';
@@ -53,6 +55,7 @@ export class TbankSyncController {
@Req() request: Request & { user: AccessPayload },
@Body(
new ValidationPipe({
forbidUnknownValues: true,
expectedType: InvestmentScreenDto,
transform: true,
whitelist: true,
@@ -65,6 +68,7 @@ export class TbankSyncController {
) {
return this.sync.request(request.user.sub, input as InvestmentScreenDto);
}
@Get(':id')
@ApiOkResponse({ type: TbankSyncStatusDto })
status(
@@ -20,6 +20,7 @@ import {
MinLength,
ValidateNested,
} from 'class-validator';
import { InvestmentResourceMetaDto } from '../investment-resource.dto';
import { operationCategories } from '../operations/operations.api';
@@ -46,12 +47,14 @@ export class InvestmentScreenDto {
@ApiProperty({ enum: investmentScreens })
@IsIn(investmentScreens)
declare screen: (typeof investmentScreens)[number];
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(maxAccountIdLength)
accountId?: string;
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
@@ -61,6 +64,7 @@ export class InvestmentScreenDto {
@IsOptional()
@IsDateString({ strict: true })
from?: string;
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
@@ -70,30 +74,36 @@ export class InvestmentScreenDto {
@IsOptional()
@IsDateString({ strict: true })
to?: string;
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
instrumentUid?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(maxQueryLength)
query?: string;
@ApiPropertyOptional({ enum: ['month', 'halfYear', 'year', 'all'] })
@IsOptional()
@IsIn(['month', 'halfYear', 'year', 'all'])
candlePeriod?: 'month' | 'halfYear' | 'year' | 'all';
candlePeriod?: 'all' | 'halfYear' | 'month' | 'year';
@ApiPropertyOptional({ type: SyncCandleRangeDto })
@IsOptional()
@ValidateNested()
@Type(() => SyncCandleRangeDto)
candleRange?: SyncCandleRangeDto;
@ApiPropertyOptional({ enum: operationCategories, isArray: true })
@IsOptional()
@IsArray()
@IsIn(operationCategories, { each: true })
categories?: (typeof operationCategories)[number][];
@ApiPropertyOptional({ minimum: 1, maximum: maxPageSize })
@IsOptional()
@IsInt()
@@ -106,6 +116,7 @@ export class IncomeInvestmentScreenDto extends PickType(InvestmentScreenDto, [
'accountId',
]) {
@ApiProperty({ enum: ['income'] }) declare screen: 'income';
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality -- Overrides optional metadata inherited through PickType.
@ApiProperty({ required: true }) declare accountId: string;
@ApiPropertyOptional({
format: 'date',
@@ -113,6 +124,7 @@ export class IncomeInvestmentScreenDto extends PickType(InvestmentScreenDto, [
'Inclusive start calendar date in Europe/Moscow (YYYY-MM-DD). Supply both from and to, or neither for the current Moscow month.',
})
from?: string;
@ApiPropertyOptional({
format: 'date',
description:
@@ -125,6 +137,7 @@ export class EventsInvestmentScreenDto extends PickType(InvestmentScreenDto, [
'accountId',
]) {
@ApiProperty({ enum: ['events'] }) declare screen: 'events';
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality -- Overrides optional metadata inherited through PickType.
@ApiProperty({ required: true }) declare accountId: string;
@ApiPropertyOptional({
format: 'date',
@@ -132,6 +145,7 @@ export class EventsInvestmentScreenDto extends PickType(InvestmentScreenDto, [
'Inclusive UTC calendar start date (YYYY-MM-DD). May be supplied independently; defaults to today in UTC.',
})
from?: string;
@ApiPropertyOptional({
format: 'date',
description:
@@ -157,11 +171,13 @@ export class TimestampInvestmentScreenDto extends OmitType(
// The runtime validator retains legacy date-only inputs; the public contract uses instants.
@ApiProperty({ enum: timestampScreens })
declare screen: (typeof timestampScreens)[number];
@ApiPropertyOptional({
format: 'date-time',
description: 'Start instant for operations.',
})
from?: string;
@ApiPropertyOptional({
format: 'date-time',
description: 'End instant for operations.',
@@ -194,6 +210,7 @@ export class TbankSyncAcceptedDto {
@ApiProperty({ format: 'uuid' }) declare id: string;
@ApiProperty({ type: Object, ...investmentScreenSchema })
declare scope: InvestmentScreenDto;
@ApiProperty({ format: 'date-time' }) declare now: string;
}
export class TbankSyncProgressDto {
@@ -205,13 +222,17 @@ export class TbankSyncStatusDto {
@ApiProperty({ format: 'uuid' }) declare id: string;
@ApiProperty({ type: Object, ...investmentScreenSchema, nullable: true })
declare scope: InvestmentScreenDto | null;
@ApiProperty({ format: 'date-time' }) declare now: string;
@ApiProperty({ enum: ['running', 'succeeded', 'failed', 'cancelled'] })
declare state: 'running' | 'succeeded' | 'failed' | 'cancelled';
declare state: 'cancelled' | 'failed' | 'running' | 'succeeded';
@ApiProperty({ type: String, nullable: true }) declare errorCode:
string | null;
null | string;
@ApiProperty({ type: TbankSyncProgressDto })
declare progress: TbankSyncProgressDto;
@ApiProperty({ type: InvestmentResourceMetaDto, isArray: true })
declare resources: InvestmentResourceMetaDto[];
}
@@ -1,16 +1,17 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../../auth/auth.module';
import { TbankModule } from '../../../integrations/tbank/tbank.module';
import { PortfolioModule } from '../portfolio/portfolio.module';
import { OperationsModule } from '../operations/operations.module';
import { InstrumentSourcesModule } from '../instruments/instrument-sources.module';
import { InstrumentsModule } from '../instruments/instruments.module';
import { AuthModule } from '../../auth/auth.module';
import { BrokerageAccountsModule } from '../brokerage-accounts/brokerage-accounts.module';
import { IncomeModule } from '../income/income.module';
import { InstrumentSourcesModule } from '../instruments/instrument-sources.module';
import { InstrumentsModule } from '../instruments/instruments.module';
import { OperationsModule } from '../operations/operations.module';
import { PortfolioModule } from '../portfolio/portfolio.module';
import { UpcomingEventsModule } from '../upcoming-events/upcoming-events.module';
import { TbankSyncController } from './tbank-sync.controller';
import { TbankSyncService } from './tbank-sync.service';
import { TbankSyncRepository } from './tbank-sync.repository';
import { TbankSyncService } from './tbank-sync.service';
@Module({
imports: [
AuthModule,
@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { createHash, randomUUID } from 'node:crypto';
import type { Prisma } from '../../../../generated/prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import type { TbankSyncAttempt } from '../../../integrations/tbank/tbank.api';
import type { InvestmentScreenDto } from './tbank-sync.dto';
export type ScreenIntent = { scope: InvestmentScreenDto; now: string };
import { PrismaService } from '../../../prisma/prisma.service';
export type ScreenIntent = { now: string; scope: InvestmentScreenDto };
export type ScreenProgress = {
children: string[];
attempts?: Record<string, string>;
children: string[];
dispatched: boolean;
};
@Injectable()
@@ -38,8 +40,9 @@ export class TbankSyncRepository {
(await tx.tbankSyncWork.count({
where: { userId, revision, id: { in: children }, state: 'running' },
}))
)
) {
return { work: previous, launch: false };
}
}
const data = {
attemptId: randomUUID(),
@@ -60,6 +63,7 @@ export class TbankSyncRepository {
return { work, launch: true };
});
}
read(userId: string, revision: string, id: string) {
return this.prisma.$transaction(async (tx) => {
const work = await tx.tbankSyncWork.findFirst({
@@ -75,19 +79,20 @@ export class TbankSyncRepository {
work,
children: children.map((child) =>
progress.attempts?.[child.id] &&
progress.attempts[child.id] !== child.attemptId
? { ...child, state: 'cancelled', errorCode: null }
: child,
progress.attempts[child.id] !== child.attemptId ?
{ ...child, state: 'cancelled', errorCode: null } :
child,
),
progress,
};
});
}
async progress(
attempt: TbankSyncAttempt,
children: string[],
dispatched: boolean,
errorCode: string | null = null,
errorCode: null | string = null,
) {
return this.prisma.$transaction(async (tx) => {
const previous = await tx.tbankSyncWork.findFirst({ where: attempt });
@@ -1,43 +1,45 @@
import {
Injectable,
ConflictException,
Injectable,
NotFoundException,
ServiceUnavailableException,
} from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { InvestmentResourceMetaDto } from '../investment-resource.dto';
import {
InstrumentMetadataService,
TbankAccessService,
TbankMembershipService,
InstrumentMetadataService,
type TbankSyncAttempt,
} from '../../../integrations/tbank/tbank.api';
import { PortfolioService } from '../portfolio/portfolio.api';
import { BrokerageAccountsService } from '../brokerage-accounts/brokerage-accounts-read.api';
import { IncomeAnalyticsService } from '../income/income-read.api';
import {
BrokerageOperationsService,
BrokerageOperationsSyncService,
type OperationInterval,
} from '../operations/operations.api';
BondIncomeService,
StockIncomeService,
} from '../instruments/instrument-income.api';
import {
CandleService,
CatalogDiscoveryService,
CatalogEnrichmentService,
} from '../instruments/instruments.api';
import {
StockIncomeService,
BondIncomeService,
} from '../instruments/instrument-income.api';
import { BrokerageAccountsService } from '../brokerage-accounts/brokerage-accounts-read.api';
import { IncomeAnalyticsService } from '../income/income-read.api';
import { UpcomingEventsService } from '../upcoming-events/upcoming-events-read.api';
import { planInvestmentScreen } from '../investment-sync-plan';
import type { InvestmentResourceMetaDto } from '../investment-resource.dto';
import {
TbankSyncRepository,
type ScreenIntent,
} from './tbank-sync.repository';
import { InvestmentScreenDto, TbankSyncStatusDto } from './tbank-sync.dto';
import { normalizeScreen } from './tbank-screen';
import { investmentResourceMeta } from '../investment-resource.mapper';
import { planInvestmentScreen } from '../investment-sync-plan';
import {
BrokerageOperationsService,
BrokerageOperationsSyncService,
type OperationInterval,
} from '../operations/operations.api';
import { PortfolioService } from '../portfolio/portfolio.api';
import { UpcomingEventsService } from '../upcoming-events/upcoming-events-read.api';
import { normalizeScreen } from './tbank-screen';
import { InvestmentScreenDto, TbankSyncStatusDto } from './tbank-sync.dto';
import {
type ScreenIntent,
TbankSyncRepository,
} from './tbank-sync.repository';
@Injectable()
export class TbankSyncService {
@@ -88,7 +90,7 @@ export class TbankSyncService {
private async dispatch(attempt: TbankSyncAttempt, intent: ScreenIntent) {
const children: string[] = [];
const add = async (ids: (string | null | undefined)[]) => {
const add = async (ids: (null | string | undefined)[]) => {
children.push(...ids.filter((id): id is string => Boolean(id)));
await this.repository.progress(attempt, children, false);
};
@@ -107,14 +109,14 @@ export class TbankSyncService {
accountScope === 'all' || account.accountId === accountScope,
);
if (
['accounts', 'account', 'events', 'stock', 'bond'].includes(
['account', 'accounts', 'bond', 'events', 'stock'].includes(
scope.screen,
)
) {
for (const account of scope.screen === 'stock' ||
scope.screen === 'bond'
? current.accounts
: selectedAccounts) {
scope.screen === 'bond' ?
current.accounts :
selectedAccounts) {
const work = await this.portfolio.request(
userId,
revision,
@@ -127,9 +129,9 @@ export class TbankSyncService {
}
const saved = await this.portfolio.read(
userId,
scope.screen === 'stock' || scope.screen === 'bond'
? 'all'
: accountScope,
scope.screen === 'stock' || scope.screen === 'bond' ?
'all' :
accountScope,
true,
);
const positions = saved.data?.positions ?? [];
@@ -137,8 +139,8 @@ export class TbankSyncService {
...new Map(
positions.flatMap((position) =>
position.instrumentUid &&
(position.type === 'STOCK' || position.type === 'BOND')
? [
(position.type === 'STOCK' || position.type === 'BOND') ?
[
[
position.instrumentUid,
{
@@ -147,27 +149,27 @@ export class TbankSyncService {
historyFrom: null,
},
] as const,
]
: [],
] :
[],
),
).values(),
];
const plan = planInvestmentScreen(
{
...scope,
candleRange: scope.candleRange
? {
candleRange: scope.candleRange ?
{
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
}
: undefined,
} :
undefined,
},
{
accounts: selectedAccounts,
instruments:
scope.screen === 'stocks' || scope.screen === 'bonds'
? []
: instruments,
scope.screen === 'stocks' || scope.screen === 'bonds' ?
[] :
instruments,
membershipKnown: Boolean(current.membershipAt),
},
now,
@@ -177,20 +179,20 @@ export class TbankSyncService {
source.kind === 'operations' &&
source.accountId &&
source.from &&
source.to
? [
source.to ?
[
{
accountId: source.accountId,
from: source.from,
to: source.to,
...(source.instrumentUid
? { instrumentUid: source.instrumentUid }
: {}),
...(source.instrumentUid ?
{ instrumentUid: source.instrumentUid } :
{}),
},
]
: [],
] :
[],
);
if (intervals.length)
if (intervals.length) {
await add([
await this.operations.request(
userId,
@@ -200,24 +202,27 @@ export class TbankSyncService {
revision,
),
]);
}
for (const source of plan.sources) {
const uid = source.instrumentUid;
const type = source.instrumentType === 'BOND' ? 'bond' : 'share';
if (source.kind === 'catalog')
if (source.kind === 'catalog') {
await add(
await this.discovery.refresh(userId, revision, {
instrumentType: type,
query: source.query ?? '',
}),
);
}
if (!uid) continue;
if (source.kind === 'metadata')
if (source.kind === 'metadata') {
await add([
await this.metadata.request(userId, revision, {
instrumentUid: uid,
}),
]);
if (source.kind === 'quote')
}
if (source.kind === 'quote') {
await add(
await this.sources.requestMany(
userId,
@@ -233,7 +238,8 @@ export class TbankSyncService {
],
),
);
if (source.kind === 'company')
}
if (source.kind === 'company') {
await add(
await this.sources.requestMany(
userId,
@@ -244,7 +250,8 @@ export class TbankSyncService {
['description', 'fundamentals'],
),
);
if (source.kind === 'metrics')
}
if (source.kind === 'metrics') {
await add(
await this.sources.requestMany(
userId,
@@ -255,7 +262,8 @@ export class TbankSyncService {
['coupons', 'yield', 'rating'],
),
);
if (source.kind === 'calendar' && source.from && source.to)
}
if (source.kind === 'calendar' && source.from && source.to) {
await add(
await this.sources.requestMany(
userId,
@@ -264,11 +272,11 @@ export class TbankSyncService {
[uid],
true,
[
type === 'share'
? 'dividends'
: scope.screen === 'bond'
? 'coupons'
: 'bond-events',
type === 'share' ?
'dividends' :
scope.screen === 'bond' ?
'coupons' :
'bond-events',
],
{
from: source.from.toISOString().slice(0, 10),
@@ -279,6 +287,7 @@ export class TbankSyncService {
},
),
);
}
}
if (
(scope.screen === 'stock' || scope.screen === 'bond') &&
@@ -292,14 +301,14 @@ export class TbankSyncService {
uid: scope.instrumentUid,
instrumentType: scope.screen === 'bond' ? 'bond' : 'share',
period: scope.candlePeriod ?? 'month',
...(scope.candleRange
? {
...(scope.candleRange ?
{
range: {
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
},
}
: {}),
} :
{}),
},
now,
true,
@@ -324,15 +333,16 @@ export class TbankSyncService {
});
const saved = await this.repository.read(userId, connection.revision, id);
if (!saved) throw new NotFoundException();
if (!saved.work.scopeKey.startsWith('screen:'))
if (!saved.work.scopeKey.startsWith('screen:')) {
return this.sourceStatus(userId, saved.work);
}
const intent = saved.work.intent as unknown as ScreenIntent;
await this.access.local(userId, intent.scope.accountId);
const children = await Promise.all(
saved.children.map((child) =>
child.state === 'superseded'
? this.operations.observe(userId, child.id)
: Promise.resolve(child),
child.state === 'superseded' ?
this.operations.observe(userId, child.id) :
Promise.resolve(child),
),
);
const running = children.some((child) => child.state === 'running');
@@ -342,21 +352,22 @@ export class TbankSyncService {
null;
const state =
saved.work.processRunId !== this.runId &&
(!saved.progress.dispatched || running)
? 'cancelled'
: !saved.progress.dispatched || running
? 'running'
: failed
? 'failed'
: children.some((child) => child.state === 'cancelled')
? 'cancelled'
: 'succeeded';
(!saved.progress.dispatched || running) ?
'cancelled' :
!saved.progress.dispatched || running ?
'running' :
failed ?
'failed' :
children.some((child) => child.state === 'cancelled') ?
'cancelled' :
'succeeded';
const resources = await this.observe(userId, intent.scope);
if (
(await this.access.local(userId, intent.scope.accountId)).revision !==
connection.revision
)
) {
throw new NotFoundException();
}
return {
id,
...intent,
@@ -380,16 +391,16 @@ export class TbankSyncService {
const add = async (read: Promise<{ meta: InvestmentResourceMetaDto }>) => {
resources.push((await read).meta);
};
if (work.scopeKey === 'membership')
if (work.scopeKey === 'membership') {
await add(this.accounts.list(userId, true));
else if (
} else if (
input.source === 'portfolio' &&
typeof input.accountId === 'string'
)
) {
await add(this.portfolio.read(userId, input.accountId, true));
else if (input.source === 'catalog') {
} else if (input.source === 'catalog') {
const type = input.instrumentType === 'bond' ? 'bond' : 'share';
if (typeof input.query === 'string')
if (typeof input.query === 'string') {
await add(
this.discovery.search(
userId,
@@ -401,14 +412,14 @@ export class TbankSyncService {
true,
),
);
else if (
} else if (
typeof input.uid === 'string' &&
typeof input.kind === 'string'
) {
const range =
typeof input.rangeKey === 'string' && input.rangeKey.startsWith('[')
? (JSON.parse(input.rangeKey) as string[])
: null;
typeof input.rangeKey === 'string' && input.rangeKey.startsWith('[') ?
(JSON.parse(input.rangeKey) as string[]) :
null;
const sources = await this.sources.readMany(
userId,
type,
@@ -417,9 +428,9 @@ export class TbankSyncService {
[
input.kind as Parameters<
CatalogEnrichmentService['readMany']
>[4] extends (infer K)[] | undefined
? K
: never,
>[4] extends (infer K)[] | undefined ?
K :
never,
],
range ? { from: range[0], to: range[1] } : undefined,
);
@@ -452,7 +463,7 @@ export class TbankSyncService {
{
uid: input.uid,
instrumentType: input.instrumentType === 'bond' ? 'bond' : 'share',
period: input.period as 'month' | 'halfYear' | 'year' | 'all',
period: input.period as 'all' | 'halfYear' | 'month' | 'year',
},
true,
),
@@ -475,21 +486,22 @@ export class TbankSyncService {
);
}
}
const observed = work.scopeKey.startsWith('operations:')
? await this.operations.observe(userId, work.id)
: work;
const observed = work.scopeKey.startsWith('operations:') ?
await this.operations.observe(userId, work.id) :
work;
const state =
observed.state === 'running'
? observed.updatedAt < this.startedAt
? 'cancelled'
: 'running'
: observed.state === 'failed'
? 'failed'
: observed.state === 'cancelled'
? 'cancelled'
: 'succeeded';
if ((await this.access.local(userId)).revision !== work.revision)
observed.state === 'running' ?
observed.updatedAt < this.startedAt ?
'cancelled' :
'running' :
observed.state === 'failed' ?
'failed' :
observed.state === 'cancelled' ?
'cancelled' :
'succeeded';
if ((await this.access.local(userId)).revision !== work.revision) {
throw new NotFoundException();
}
return {
id: work.id,
scope: null,
@@ -511,14 +523,15 @@ export class TbankSyncService {
resources.push((await read).meta);
};
const account = scope.accountId ?? 'all';
if (scope.screen === 'accounts')
if (scope.screen === 'accounts') {
await add(this.accounts.list(userId, true));
}
if (scope.screen === 'account') {
await add(this.portfolio.read(userId, account, true));
await add(this.income.summary(userId, account, true));
await add(this.events.get(userId, account, { sync: 'observe' }));
}
if (['account', 'operations', 'stock', 'bond'].includes(scope.screen))
if (['account', 'bond', 'operations', 'stock'].includes(scope.screen)) {
await add(
this.ledger.read(userId, account, {
from: scope.from,
@@ -529,11 +542,13 @@ export class TbankSyncService {
sync: 'observe',
}),
);
if (scope.screen === 'income')
}
if (scope.screen === 'income') {
await add(
this.income.analytics(userId, account, scope.from, scope.to, true),
);
if (scope.screen === 'events')
}
if (scope.screen === 'events') {
await add(
this.events.get(userId, account, {
from: scope.from,
@@ -541,9 +556,10 @@ export class TbankSyncService {
sync: 'observe',
}),
);
}
const type =
scope.screen === 'bond' || scope.screen === 'bonds' ? 'bond' : 'share';
if (scope.screen === 'stocks' || scope.screen === 'bonds')
if (scope.screen === 'stocks' || scope.screen === 'bonds') {
await add(
this.discovery.search(
userId,
@@ -551,6 +567,7 @@ export class TbankSyncService {
true,
),
);
}
if (
(scope.screen === 'stock' || scope.screen === 'bond') &&
scope.instrumentUid
@@ -574,27 +591,27 @@ export class TbankSyncService {
uid,
instrumentType: type,
period: scope.candlePeriod ?? 'month',
...(scope.candleRange
? {
...(scope.candleRange ?
{
range: {
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
},
}
: {}),
} :
{}),
},
true,
),
);
await add(
type === 'share'
? this.stockIncome.readPosition(userId, uid, true)
: this.bondIncome.readPosition(userId, uid, true),
type === 'share' ?
this.stockIncome.readPosition(userId, uid, true) :
this.bondIncome.readPosition(userId, uid, true),
);
await add(
type === 'share'
? this.stockIncome.dividends(userId, uid, true)
: this.bondIncome.coupons(userId, uid, true),
type === 'share' ?
this.stockIncome.dividends(userId, uid, true) :
this.bondIncome.coupons(userId, uid, true),
);
}
return resources;