Files
backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts
T

242 lines
6.7 KiB
TypeScript

import {
ApiProperty,
ApiPropertyOptional,
getSchemaPath,
OmitType,
PickType,
} from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsDateString,
IsIn,
IsInt,
IsOptional,
IsString,
IsUUID,
Max,
MaxLength,
Min,
MinLength,
ValidateNested,
} from 'class-validator';
import { MAX_INSTRUMENT_QUERY_LENGTH, MIN_INSTRUMENT_QUERY_LENGTH } from '../instrument-query';
import { InvestmentResourceMetaDto } from '../investment-resource.dto';
import { operationCategories } from '../operations/operations.api';
const MIN_ACCOUNT_ID_LENGTH = 1;
const MIN_SYNC_PAGE_SIZE = 1;
const maxAccountIdLength = 256;
const maxPageSize = 1000;
export const investmentScreens = [
'accounts',
'account',
'operations',
'income',
'events',
'stocks',
'bonds',
'stock',
'bond',
] as const;
export class SyncCandleRangeDto {
@ApiProperty({ format: 'date-time' }) @IsDateString() declare from: string;
@ApiProperty({ format: 'date-time' }) @IsDateString() declare to: string;
}
export class InvestmentScreenDto {
@ApiProperty({ enum: investmentScreens })
@IsIn(investmentScreens)
declare screen: (typeof investmentScreens)[number];
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MinLength(MIN_ACCOUNT_ID_LENGTH)
@MaxLength(maxAccountIdLength)
accountId?: string;
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
{ type: 'string', format: 'date-time' },
],
})
@IsOptional()
@IsDateString({ strict: true })
from?: string;
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
{ type: 'string', format: 'date-time' },
],
})
@IsOptional()
@IsDateString({ strict: true })
to?: string;
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
instrumentUid?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MinLength(MIN_INSTRUMENT_QUERY_LENGTH)
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
query?: string;
@ApiPropertyOptional({ enum: ['month', 'halfYear', 'year', 'all'] })
@IsOptional()
@IsIn(['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: MIN_SYNC_PAGE_SIZE, maximum: maxPageSize })
@IsOptional()
@IsInt()
@Min(MIN_SYNC_PAGE_SIZE)
@Max(maxPageSize)
limit?: number;
}
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',
description:
'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:
'Inclusive end calendar date in Europe/Moscow (YYYY-MM-DD). Supply both from and to, or neither. Timestamp queries use the start of the following Moscow day as the exclusive end, capped at now.',
})
to?: string;
}
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',
description:
'Inclusive UTC calendar start date (YYYY-MM-DD). May be supplied independently; defaults to today in UTC.',
})
from?: string;
@ApiPropertyOptional({
format: 'date',
description:
'Inclusive UTC calendar end date (YYYY-MM-DD). May be supplied independently; defaults to 30 days after from. Timestamp queries use the start of the following UTC day as the exclusive end.',
})
to?: string;
}
const timestampScreens = [
'accounts',
'account',
'operations',
'stocks',
'bonds',
'stock',
'bond',
] as const;
export class TimestampInvestmentScreenDto extends OmitType(
InvestmentScreenDto,
['screen', 'from', 'to'],
) {
// 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.',
})
to?: string;
}
export const investmentScreenSchema = {
oneOf: [
IncomeInvestmentScreenDto,
EventsInvestmentScreenDto,
TimestampInvestmentScreenDto,
].map((model) => ({ $ref: getSchemaPath(model) })),
discriminator: {
propertyName: 'screen',
mapping: {
income: getSchemaPath(IncomeInvestmentScreenDto),
events: getSchemaPath(EventsInvestmentScreenDto),
...Object.fromEntries(
timestampScreens.map((screen) => [
screen,
getSchemaPath(TimestampInvestmentScreenDto),
]),
),
},
},
};
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 {
@ApiProperty() declare completed: number;
@ApiProperty() declare total: number;
@ApiProperty() declare discovering: boolean;
}
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: 'cancelled' | 'failed' | 'running' | 'succeeded';
@ApiProperty({ type: String, nullable: true }) declare errorCode:
null | string;
@ApiProperty({ type: TbankSyncProgressDto })
declare progress: TbankSyncProgressDto;
@ApiProperty({ type: InvestmentResourceMetaDto, isArray: true })
declare resources: InvestmentResourceMetaDto[];
}