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
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:
@@ -62,4 +62,13 @@ describe('manual sync screen boundary', () => {
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
it('keeps independently supplied event dates and rejects an end before the default UTC start', () => {
|
||||
for (const range of [{ from: '2096-02-29' }, { to: '2096-02-29' }]) {
|
||||
const scope = { screen: 'events' as const, accountId: 'all', ...range };
|
||||
expect(normalizeScreen(scope)).toEqual(scope);
|
||||
}
|
||||
expect(() =>
|
||||
normalizeScreen({ screen: 'events', accountId: 'all', to: '2000-01-01' }),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,12 +63,19 @@ export function normalizeScreen(
|
||||
if (['stock', 'bond'].includes(input.screen) && !input.instrumentUid) fail();
|
||||
if (input.accountId !== undefined && !accountId) fail();
|
||||
if (['income', 'events'].includes(input.screen)) {
|
||||
if (Boolean(input.from) !== Boolean(input.to)) fail();
|
||||
if (input.screen === 'income' && Boolean(input.from) !== Boolean(input.to))
|
||||
fail();
|
||||
for (const value of [input.from, input.to])
|
||||
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) fail();
|
||||
}
|
||||
if (input.from && input.to && new Date(input.from) > new Date(input.to))
|
||||
fail();
|
||||
if (
|
||||
input.screen === 'events' &&
|
||||
input.to &&
|
||||
(input.from ?? new Date().toISOString().slice(0, 10)) > input.to
|
||||
)
|
||||
fail();
|
||||
if (
|
||||
input.candleRange &&
|
||||
new Date(input.candleRange.from) >= new Date(input.candleRange.to)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ApiAcceptedResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiExtraModels,
|
||||
ApiOkResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
@@ -24,12 +25,21 @@ import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-r
|
||||
import { AccessPayload, JwtAuthGuard } from '../../auth/auth.api';
|
||||
import {
|
||||
InvestmentScreenDto,
|
||||
IncomeInvestmentScreenDto,
|
||||
EventsInvestmentScreenDto,
|
||||
TimestampInvestmentScreenDto,
|
||||
investmentScreenSchema,
|
||||
TbankSyncAcceptedDto,
|
||||
TbankSyncStatusDto,
|
||||
} from './tbank-sync.dto';
|
||||
import { TbankSyncService } from './tbank-sync.service';
|
||||
|
||||
@ApiTags('tbank-sync')
|
||||
@ApiExtraModels(
|
||||
IncomeInvestmentScreenDto,
|
||||
EventsInvestmentScreenDto,
|
||||
TimestampInvestmentScreenDto,
|
||||
)
|
||||
@ApiProblemResponses()
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -38,7 +48,7 @@ export class TbankSyncController {
|
||||
constructor(private readonly sync: TbankSyncService) {}
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.ACCEPTED)
|
||||
@ApiBody({ type: InvestmentScreenDto })
|
||||
@ApiBody({ schema: investmentScreenSchema })
|
||||
@ApiAcceptedResponse({ type: TbankSyncAcceptedDto })
|
||||
request(
|
||||
@Req() request: Request & { user: AccessPayload },
|
||||
|
||||
@@ -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'] })
|
||||
|
||||
Reference in New Issue
Block a user