import { ApiProperty, ApiPropertyOptional, 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'; const trim = ({ value }: { value: unknown }) => typeof value === 'string' ? value.trim() : value; export const bankOperationKinds = [ 'expense', 'topup', 'transfer', 'refund', ] as const; export type BankOperationKind = (typeof bankOperationKinds)[number]; export class RubMoneyInputDto { @ApiProperty({ enum: ['RUB'] }) @IsIn(['RUB']) currency: string; @ApiProperty({ pattern: '^-?[0-9]+(\\.[0-9]{1,2})?$' }) @IsString() @Matches(/^-?\d+(\.\d{1,2})?$/) value: string; } export class CreateBankCardDto { @ApiProperty({ format: 'uuid' }) @IsUUID() id: string; @ApiProperty({ minLength: 1, maxLength: 100 }) @Transform(trim) @IsString() @MinLength(1) @MaxLength(100) name: string; @ApiPropertyOptional({ type: String, nullable: true, maxLength: 100 }) @IsOptional() @Transform(trim) @IsString() @MaxLength(100) bankName?: string | null; @ApiPropertyOptional({ type: String, nullable: true, pattern: '^[0-9]{4}$' }) @IsOptional() @Matches(/^[0-9]{4}$/) lastFour?: string | null; @ApiProperty({ type: RubMoneyInputDto }) @IsDefined() @ValidateNested() @Type(() => RubMoneyInputDto) openingBalance: MoneyDto; @ApiProperty({ format: 'date' }) @IsString() @Matches(/^\d{4}-\d{2}-\d{2}$/) openingDate: string; } export class UpdateBankCardDto extends PartialType( OmitType(CreateBankCardDto, ['id'] as const), { skipNullProperties: false }, ) { @ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number; @ApiPropertyOptional() @ValidateIf((_object, value: unknown) => value !== undefined) @IsBoolean() archived?: boolean; } export class CategoryNameDto { @ApiProperty({ minLength: 1, maxLength: 80 }) @Transform(trim) @IsString() @MinLength(1) @MaxLength(80) name: string; } export class UpdateExpenseCategoryDto extends CategoryNameDto { @ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number; } export class CreateBankOperationDto { @ApiProperty({ format: 'uuid' }) @IsUUID() id: string; @ApiProperty({ enum: bankOperationKinds }) @IsIn(bankOperationKinds) kind: BankOperationKind; @ApiProperty({ format: 'uuid' }) @IsUUID() 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) amount: MoneyDto; @ApiProperty({ format: 'date' }) @IsString() @Matches(/^\d{4}-\d{2}-\d{2}$/) date: string; @ApiPropertyOptional({ maxLength: 500 }) @ValidateIf((_object, value: unknown) => value !== undefined) @IsString() @MaxLength(500) description?: string; } export class UpdateBankOperationDto extends PartialType( OmitType(CreateBankOperationDto, ['id'] as const), { skipNullProperties: false }, ) { @ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number; } export class BankCardsQueryDto { @ApiPropertyOptional({ enum: ['true', 'false'] }) @IsOptional() @IsIn(['true', 'false']) includeArchived?: string; } export class BankPageQueryDto { @ApiPropertyOptional({ minimum: 1, default: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number; @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 50 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number; } export class BankOperationsQueryDto extends BankPageQueryDto { @ApiProperty({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' }) @Matches(/^\d{4}-(0[1-9]|1[0-2])$/) month: string; } export class BankSummaryQueryDto { @ApiProperty({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' }) @Matches(/^\d{4}-(0[1-9]|1[0-2])$/) month: string; @ApiPropertyOptional({ format: 'uuid' }) @IsOptional() @IsUUID() cardId?: string; } export class BankVersionQueryDto { @ApiProperty({ minimum: 1 }) @Type(() => Number) @IsInt() @Min(1) version: number; } export class ExpenseCategoryDto { @ApiProperty({ type: String }) id: string; @ApiProperty({ type: String }) name: string; @ApiProperty({ type: Boolean }) isDefault: boolean; @ApiProperty({ type: Number }) version: number; } export class BankCardDto { @ApiProperty({ type: String }) id: string; @ApiProperty({ type: String }) name: string; @ApiProperty({ type: String, nullable: true }) bankName: string | null; @ApiProperty({ type: String, nullable: true }) lastFour: string | null; @ApiProperty({ type: MoneyDto }) openingBalance: MoneyDto; @ApiProperty({ type: String }) openingDate: string; @ApiProperty({ type: MoneyDto }) balance: MoneyDto; @ApiProperty({ type: String, nullable: true }) archivedAt: string | null; @ApiProperty({ type: Number }) version: number; } export class BankOperationDto { @ApiProperty({ type: String }) id: string; @ApiProperty({ enum: bankOperationKinds }) kind: BankOperationKind; @ApiProperty({ type: String }) cardId: string; @ApiProperty({ type: String }) cardName: string; @ApiProperty({ type: String, nullable: true }) targetCardId: string | null; @ApiProperty({ type: String, nullable: true }) targetCardName: string | null; @ApiProperty({ type: String, nullable: true }) categoryId: string | null; @ApiProperty({ type: String, nullable: true }) categoryName: string | null; @ApiProperty({ type: String, nullable: true }) originalExpenseId: string | null; @ApiProperty({ type: MoneyDto }) amount: MoneyDto; @ApiProperty({ type: MoneyDto, nullable: true }) availableRefund: MoneyDto | null; @ApiProperty({ type: String }) date: string; @ApiProperty({ type: String, nullable: true }) description: string | null; @ApiProperty({ type: Number }) version: number; @ApiProperty({ type: String }) createdAt: string; } export class BankOperationsPageDto { @ApiProperty({ type: BankOperationDto, isArray: true }) items: BankOperationDto[]; @ApiProperty({ type: Number }) page: number; @ApiProperty({ type: Number }) total: number; @ApiProperty({ type: Boolean }) hasNext: boolean; } export class BankSummaryCategoryDto { @ApiProperty({ type: String }) categoryId: string; @ApiProperty({ type: String }) categoryName: string; @ApiProperty({ type: MoneyDto }) expenses: MoneyDto; @ApiProperty({ type: MoneyDto }) refunds: MoneyDto; @ApiProperty({ type: MoneyDto }) net: MoneyDto; } export class BankSummaryDto { @ApiProperty({ type: String }) month: string; @ApiProperty({ type: String, nullable: true }) cardId: string | null; @ApiProperty({ type: MoneyDto }) expenses: MoneyDto; @ApiProperty({ type: MoneyDto }) refunds: MoneyDto; @ApiProperty({ type: MoneyDto }) net: MoneyDto; @ApiProperty({ type: BankSummaryCategoryDto, isArray: true }) categories: BankSummaryCategoryDto[]; }