refactor: organize backend by business responsibility

This commit is contained in:
2026-09-13 13:52:57 +03:00
parent 7fde3ece6e
commit 8ba005346d
145 changed files with 3027 additions and 2346 deletions
@@ -0,0 +1,138 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsDateString,
IsEnum,
IsInt,
IsOptional,
IsString,
IsUUID,
Max,
MaxLength,
Min,
} from 'class-validator';
import { MoneyDto } from '../../../../common/money.dto';
import { AccountReferenceDto } from '../../account-reference.dto';
export const operationCategories = [
'trade',
'income',
'tax',
'fee',
'transfer',
'other',
] as const;
export type OperationCategory = (typeof operationCategories)[number];
export class BrokerageOperationsQueryDto {
@ApiPropertyOptional({
format: 'uuid',
description:
'Instrument UID. Without from, includes history since the earliest available account opening.',
})
@IsOptional()
@IsUUID()
instrumentUid?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
from?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
to?: string;
@ApiPropertyOptional({ enum: operationCategories, isArray: true })
@IsOptional()
@Transform(({ value }: { value: unknown }) =>
typeof value === 'string' ? value.split(',').filter(Boolean) : value,
)
@IsArray()
@IsEnum(operationCategories, { each: true })
categories?: OperationCategory[];
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(4096)
cursor?: string;
@ApiPropertyOptional({ minimum: 1, maximum: 1000, default: 100 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(1000)
limit?: number;
}
export class BrokerageOperationDto {
@ApiPropertyOptional({ type: AccountReferenceDto })
account?: AccountReferenceDto;
@ApiProperty()
declare id: string;
@ApiProperty({ format: 'date-time' })
declare occurredAt: string;
@ApiProperty()
declare type: string;
@ApiProperty({ enum: operationCategories })
declare category: OperationCategory;
@ApiProperty()
declare description: string;
@ApiProperty({ type: String, nullable: true })
declare figi: string | null;
@ApiProperty({ type: String, nullable: true })
declare instrumentUid: string | null;
@ApiProperty({ type: String, nullable: true })
declare ticker: string | null;
@ApiProperty({ type: String, nullable: true })
declare instrumentType: string | null;
@ApiProperty({ type: String, nullable: true })
declare logoName: string | null;
@ApiProperty({ type: MoneyDto, nullable: true })
declare payment: MoneyDto | null;
@ApiProperty({ type: String, nullable: true })
declare quantity: string | null;
@ApiProperty({ type: MoneyDto, nullable: true })
declare price: MoneyDto | null;
@ApiProperty({ type: MoneyDto, nullable: true })
declare accruedInterest: MoneyDto | null;
@ApiProperty({ type: MoneyDto, nullable: true })
declare commission: MoneyDto | null;
}
export class BrokerageOperationsPageDto {
@ApiProperty()
declare accountId: string;
@ApiProperty({ type: BrokerageOperationDto, isArray: true })
declare items: BrokerageOperationDto[];
@ApiProperty({ type: String, nullable: true })
declare nextCursor: string | null;
@ApiProperty()
declare hasNext: boolean;
@ApiProperty({ format: 'date-time' })
declare observedAt: string;
}
@@ -0,0 +1,18 @@
jest.mock('../../../../prisma/prisma.service', () => ({
PrismaService: class PrismaService {},
}));
import { BrokerageOperationsController } from './brokerage-operations.controller';
describe('BrokerageOperationsController', () => {
it('passes the authenticated user, account, and validated query to the service', async () => {
const operations = { get: jest.fn() };
const controller = new BrokerageOperationsController(operations as never);
const query = { limit: 10 };
const request = { user: { sub: 'user' } };
await controller.get(request as never, 'account', query);
expect(operations.get).toHaveBeenCalledWith('user', 'account', query);
});
});
@@ -0,0 +1,38 @@
import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import type { Request } from 'express';
import { ApiProblemResponses } from '../../../../common/decorators/api-safe-error-response.decorator';
import { AccessPayload, JwtAuthGuard } from '../../../auth/auth.api';
import {
BrokerageOperationsPageDto,
BrokerageOperationsQueryDto,
} from './brokerage-operation.dto';
import { BrokerageOperationsService } from '../application/brokerage-operations.service';
@ApiTags('brokerage-accounts')
@ApiProblemResponses()
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('brokerage-accounts')
export class BrokerageOperationsController {
constructor(private readonly operations: BrokerageOperationsService) {}
@Get('all/operations')
@ApiOkResponse({ type: BrokerageOperationsPageDto })
all(
@Req() request: Request & { user: AccessPayload },
@Query() query: BrokerageOperationsQueryDto,
) {
return this.operations.getAll(request.user.sub, query);
}
@Get(':accountId/operations')
@ApiOkResponse({ type: BrokerageOperationsPageDto })
get(
@Req() request: Request & { user: AccessPayload },
@Param('accountId') accountId: string,
@Query() query: BrokerageOperationsQueryDto,
) {
return this.operations.get(request.user.sub, accountId, query);
}
}