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 { 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 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; }