273 lines
7.4 KiB
TypeScript
273 lines
7.4 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 { 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;
|
|
}
|
|
|
|
export class HourlySyncSectionDto {
|
|
@ApiProperty({ enum: ['membership', 'portfolio', 'operations', 'events'] })
|
|
declare section: 'events' | 'membership' | 'operations' | 'portfolio';
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: false,
|
|
description: 'Account identifier of the problematic section, when scoped.',
|
|
})
|
|
declare accountId?: string;
|
|
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
description:
|
|
'Safe error code of the failed section. No tokens or upstream payloads.',
|
|
})
|
|
declare errorCode: null | string;
|
|
}
|
|
|
|
export class HourlySyncStatusDto {
|
|
@ApiProperty({
|
|
enum: ['running', 'succeeded', 'partial', 'failed', 'cancelled'],
|
|
description:
|
|
'Last hourly walk state. Cancelled means an interrupted attempt; its start still delays the next term.',
|
|
})
|
|
declare state:
|
|
| 'cancelled' |
|
|
'failed' |
|
|
'partial' |
|
|
'running' |
|
|
'succeeded';
|
|
|
|
@ApiProperty({ format: 'date-time' })
|
|
declare startedAt: string;
|
|
|
|
@ApiProperty({ type: String, nullable: true, format: 'date-time' })
|
|
declare finishedAt: null | string;
|
|
|
|
@ApiProperty({ type: HourlySyncSectionDto, isArray: true })
|
|
declare sections: HourlySyncSectionDto[];
|
|
}
|