fix(investments): align roles and date contracts
Backend security / security (push) Successful in 9s
Backend verify / verify (push) Successful in 10m23s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Successful in 10m39s
Conventional commits / commits (pull_request) Successful in 29s

Identify income dependencies by role and document screen-specific dates.
Convert inclusive calendar days into half-open source ranges while
preserving operation timestamps and existing stored coverage.
This commit is contained in:
2026-09-27 11:38:13 +03:00
parent e4232138d0
commit 516e43203e
18 changed files with 500 additions and 74 deletions
@@ -1,4 +1,10 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
ApiProperty,
ApiPropertyOptional,
getSchemaPath,
OmitType,
PickType,
} from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
@@ -46,11 +52,21 @@ export class InvestmentScreenDto {
@MinLength(1)
@MaxLength(maxAccountIdLength)
accountId?: string;
@ApiPropertyOptional({ format: 'date-time' })
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
{ type: 'string', format: 'date-time' },
],
})
@IsOptional()
@IsDateString({ strict: true })
from?: string;
@ApiPropertyOptional({ format: 'date-time' })
@ApiPropertyOptional({
oneOf: [
{ type: 'string', format: 'date' },
{ type: 'string', format: 'date-time' },
],
})
@IsOptional()
@IsDateString({ strict: true })
to?: string;
@@ -85,9 +101,98 @@ export class InvestmentScreenDto {
@Max(maxPageSize)
limit?: number;
}
export class IncomeInvestmentScreenDto extends PickType(InvestmentScreenDto, [
'accountId',
]) {
@ApiProperty({ enum: ['income'] }) declare screen: 'income';
@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';
@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: InvestmentScreenDto })
@ApiProperty({ type: Object, ...investmentScreenSchema })
declare scope: InvestmentScreenDto;
@ApiProperty({ format: 'date-time' }) declare now: string;
}
@@ -98,7 +203,7 @@ export class TbankSyncProgressDto {
}
export class TbankSyncStatusDto {
@ApiProperty({ format: 'uuid' }) declare id: string;
@ApiProperty({ type: InvestmentScreenDto, nullable: true })
@ApiProperty({ type: Object, ...investmentScreenSchema, nullable: true })
declare scope: InvestmentScreenDto | null;
@ApiProperty({ format: 'date-time' }) declare now: string;
@ApiProperty({ enum: ['running', 'succeeded', 'failed', 'cancelled'] })