chore: strengthen backend eslint profile
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user