Files
backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts
T
codex-bot 96d4de4195
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 12s
Backend verify / verify (push) Failing after 5m47s
refactor: remove unreachable catalog producer and sync orchestration (WealthLedger/backend#62) (#98)
2026-10-15 10:46:58 +03:00

247 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 { InvestmentAttemptProblemDto } 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 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 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: InvestmentAttemptProblemDto, isArray: true })
declare problems: InvestmentAttemptProblemDto[];
}