import { ApiProperty, ApiPropertyOptional, IntersectionType, OmitType, PartialType, } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsDefined, IsIn, IsInt, IsOptional, IsString, IsUUID, Matches, Max, MaxLength, Min, MinLength, ValidateIf, ValidateNested, } from 'class-validator'; import { MoneyDto } from '../../../../common/money.dto'; import { FIRST_PAGE, MAX_CARD_NAME_LENGTH, MAX_CATEGORY_NAME_LENGTH, MAX_OPERATION_DESCRIPTION_LENGTH, MIN_CARD_NAME_LENGTH, MIN_CATEGORY_NAME_LENGTH, MIN_ENTITY_VERSION, } from '../domain/bank-card-limits'; import { BankOperationKind } from '../domain/bank-operation-kind'; const MIN_PAGE_SIZE = 1; export const DEFAULT_BANK_PAGE_SIZE = 50; const MAX_BANK_PAGE_SIZE = 100; const trim = ({ value }: { value: unknown }) => typeof value === 'string' ? value.trim() : value; export const bankPaymentSystems = ['mir', 'visa', 'mastercard'] as const; const bankOperationKinds = Object.values(BankOperationKind); export class RubMoneyInputDto { @ApiProperty({ enum: ['RUB'] }) @IsIn(['RUB']) declare currency: string; @ApiProperty({ pattern: '^-?[0-9]+(\\.[0-9]{1,2})?$' }) @IsString() // eslint-disable-next-line security/detect-unsafe-regex -- Anchored decimal grammar: the literal decimal point separates disjoint digit runs; backtracking is linear. @Matches(/^-?\d+(\.\d{1,2})?$/) declare value: string; } export class CreateBankCardDto { @ApiProperty({ format: 'uuid' }) @IsUUID() declare id: string; @ApiProperty({ minLength: MIN_CARD_NAME_LENGTH, maxLength: MAX_CARD_NAME_LENGTH }) @Transform(trim) @IsString() @MinLength(MIN_CARD_NAME_LENGTH) @MaxLength(MAX_CARD_NAME_LENGTH) declare name: string; @ApiPropertyOptional({ type: String, nullable: true, maxLength: MAX_CARD_NAME_LENGTH, }) @IsOptional() @Transform(trim) @IsString() @MaxLength(MAX_CARD_NAME_LENGTH) bankName?: null | string; @ApiPropertyOptional({ type: String, nullable: true, pattern: '^[0-9]{4}$' }) @IsOptional() @Matches(/^\d{4}$/) lastFour?: null | string; @ApiPropertyOptional({ enum: bankPaymentSystems, nullable: true }) @IsOptional() @IsIn(bankPaymentSystems) paymentSystem?: null | string; @ApiProperty({ type: RubMoneyInputDto }) @IsDefined() @ValidateNested() @Type(() => RubMoneyInputDto) declare openingBalance: MoneyDto; @ApiProperty({ format: 'date' }) @IsString() @Matches(/^\d{4}-\d{2}-\d{2}$/) declare openingDate: string; } export class UpdateBankCardDto extends PartialType( OmitType(CreateBankCardDto, ['id'] as const), { skipNullProperties: false }, ) { @ApiProperty({ minimum: MIN_ENTITY_VERSION }) @IsInt() @Min(MIN_ENTITY_VERSION) declare version: number; @ApiPropertyOptional() @ValidateIf((_object, value: unknown) => value !== undefined) @IsBoolean() archived?: boolean; } export class CategoryNameDto { @ApiProperty({ minLength: MIN_CATEGORY_NAME_LENGTH, maxLength: MAX_CATEGORY_NAME_LENGTH }) @Transform(trim) @IsString() @MinLength(MIN_CATEGORY_NAME_LENGTH) @MaxLength(MAX_CATEGORY_NAME_LENGTH) declare name: string; } export class UpdateExpenseCategoryDto extends CategoryNameDto { @ApiProperty({ minimum: MIN_ENTITY_VERSION }) @IsInt() @Min(MIN_ENTITY_VERSION) declare version: number; } export class CreateBankOperationDto { @ApiProperty({ format: 'uuid' }) @IsUUID() declare id: string; @ApiProperty({ enum: bankOperationKinds }) @IsIn(bankOperationKinds) declare kind: BankOperationKind; @ApiProperty({ format: 'uuid' }) @IsUUID() declare cardId: string; @ApiPropertyOptional({ format: 'uuid' }) @ValidateIf((_object, value: unknown) => value !== undefined) @IsUUID() targetCardId?: string; @ApiPropertyOptional({ format: 'uuid' }) @ValidateIf((_object, value: unknown) => value !== undefined) @IsUUID() categoryId?: string; @ApiPropertyOptional({ format: 'uuid' }) @ValidateIf((_object, value: unknown) => value !== undefined) @IsUUID() originalExpenseId?: string; @ApiProperty({ type: RubMoneyInputDto }) @IsDefined() @ValidateNested() @Type(() => RubMoneyInputDto) declare amount: MoneyDto; @ApiProperty({ format: 'date' }) @IsString() @Matches(/^\d{4}-\d{2}-\d{2}$/) declare date: string; @ApiPropertyOptional({ maxLength: MAX_OPERATION_DESCRIPTION_LENGTH }) @ValidateIf((_object, value: unknown) => value !== undefined) @IsString() @MaxLength(MAX_OPERATION_DESCRIPTION_LENGTH) description?: string; } export class UpdateBankOperationDto extends PartialType( OmitType(CreateBankOperationDto, ['id'] as const), { skipNullProperties: false }, ) { @ApiProperty({ minimum: MIN_ENTITY_VERSION }) @IsInt() @Min(MIN_ENTITY_VERSION) declare version: number; } export class BankCardsQueryDto { @ApiPropertyOptional({ enum: ['true', 'false'] }) @IsOptional() @IsIn(['true', 'false']) includeArchived?: string; } export class BankPageQueryDto { @ApiPropertyOptional({ minimum: FIRST_PAGE, default: FIRST_PAGE }) @IsOptional() @Type(() => Number) @IsInt() @Min(FIRST_PAGE) page?: number; @ApiPropertyOptional({ minimum: MIN_PAGE_SIZE, maximum: MAX_BANK_PAGE_SIZE, default: DEFAULT_BANK_PAGE_SIZE, }) @IsOptional() @Type(() => Number) @IsInt() @Min(MIN_PAGE_SIZE) @Max(MAX_BANK_PAGE_SIZE) limit?: number; } export class BankPeriodQueryDto { @ApiPropertyOptional({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$', description: 'Use month or both from and to, exclusively.', }) @IsOptional() @Matches(/^\d{4}-(0[1-9]|1[0-2])$/) month?: string; @ApiPropertyOptional({ format: 'date', description: 'Inclusive start date; requires to.', }) @IsOptional() @Matches(/^\d{4}-\d{2}-\d{2}$/) from?: string; @ApiPropertyOptional({ format: 'date', description: 'Inclusive end date; requires from.', }) @IsOptional() @Matches(/^\d{4}-\d{2}-\d{2}$/) to?: string; } export class BankOperationsQueryDto extends IntersectionType( BankPageQueryDto, BankPeriodQueryDto, ) {} export class BankSummaryQueryDto extends BankPeriodQueryDto { @ApiPropertyOptional({ format: 'uuid' }) @IsOptional() @IsUUID() cardId?: string; } export class BankVersionQueryDto { @ApiProperty({ minimum: MIN_ENTITY_VERSION }) @Type(() => Number) @IsInt() @Min(MIN_ENTITY_VERSION) declare version: number; } export class ExpenseCategoryDto { @ApiProperty({ type: String }) declare id: string; @ApiProperty({ type: String }) declare name: string; @ApiProperty({ type: Boolean }) declare isDefault: boolean; @ApiProperty({ type: Number }) declare version: number; } export class BankCardDto { @ApiProperty({ type: String }) declare id: string; @ApiProperty({ type: String }) declare name: string; @ApiProperty({ type: String, nullable: true }) declare bankName: null | string; @ApiProperty({ type: String, nullable: true }) declare lastFour: null | string; @ApiProperty({ enum: bankPaymentSystems, nullable: true }) declare paymentSystem: null | string; @ApiProperty({ type: MoneyDto }) declare openingBalance: MoneyDto; @ApiProperty({ type: String }) declare openingDate: string; @ApiProperty({ type: MoneyDto }) declare balance: MoneyDto; @ApiProperty({ type: String, nullable: true }) declare archivedAt: null | string; @ApiProperty({ type: Number }) declare version: number; } export class BankOperationDto { @ApiProperty({ type: String }) declare id: string; @ApiProperty({ enum: bankOperationKinds }) declare kind: BankOperationKind; @ApiProperty({ type: String }) declare cardId: string; @ApiProperty({ type: String }) declare cardName: string; @ApiProperty({ type: String, nullable: true }) declare targetCardId: null | string; @ApiProperty({ type: String, nullable: true }) declare targetCardName: null | string; @ApiProperty({ type: String, nullable: true }) declare categoryId: null | string; @ApiProperty({ type: String, nullable: true }) declare categoryName: null | string; @ApiProperty({ type: String, nullable: true }) declare originalExpenseId: null | string; @ApiProperty({ type: MoneyDto }) declare amount: MoneyDto; @ApiProperty({ type: MoneyDto, nullable: true }) declare availableRefund: MoneyDto | null; @ApiProperty({ type: String }) declare date: string; @ApiProperty({ type: String, nullable: true }) declare description: null | string; @ApiProperty({ type: Number }) declare version: number; @ApiProperty({ type: String }) declare createdAt: string; } export class BankOperationsPageDto { @ApiProperty({ type: BankOperationDto, isArray: true }) declare items: BankOperationDto[]; @ApiProperty({ type: Number }) declare page: number; @ApiProperty({ type: Number }) declare total: number; @ApiProperty({ type: Boolean }) declare hasNext: boolean; } export class BankSummaryCategoryDto { @ApiProperty({ type: String }) declare categoryId: string; @ApiProperty({ type: String }) declare categoryName: string; @ApiProperty({ type: MoneyDto }) declare expenses: MoneyDto; @ApiProperty({ type: MoneyDto }) declare refunds: MoneyDto; @ApiProperty({ type: MoneyDto }) declare net: MoneyDto; } export class BankSummaryDto { @ApiPropertyOptional({ type: String }) month?: string; @ApiPropertyOptional({ format: 'date' }) from?: string; @ApiPropertyOptional({ format: 'date' }) to?: string; @ApiProperty({ type: String, nullable: true }) declare cardId: null | string; @ApiProperty({ type: MoneyDto }) declare expenses: MoneyDto; @ApiProperty({ type: MoneyDto }) declare refunds: MoneyDto; @ApiProperty({ type: MoneyDto }) declare net: MoneyDto; @ApiProperty({ type: BankSummaryCategoryDto, isArray: true }) declare categories: BankSummaryCategoryDto[]; }