feat(validators): agregar validadores con mensajes en español a 12 DTOs adicionales

Se completaron validadores en los siguientes DTOs:
- storage: GetUploadUrlDto, ConfirmUploadDto, ListFilesDto, UpdateFileDto
- billing: CreatePaymentMethodDto, CreateCheckoutSessionDto, CreateBillingPortalSessionDto
- email: EmailAddressDto, AttachmentDto, SendEmailDto, SendTemplateEmailDto, BulkSendEmailDto
- rbac: CreateRoleDto, UpdateRoleDto, AssignRoleDto
- sales: CreateActivityDto, UpdateActivityDto, ActivityListQueryDto
- sales: CreatePipelineStageDto, UpdatePipelineStageDto, ReorderStagesDto
- commissions: CreateAssignmentDto, UpdateAssignmentDto, AssignmentListQueryDto
- commissions: CreatePeriodDto, UpdatePeriodDto, ClosePeriodDto, MarkPaidDto, PeriodListQueryDto
- portfolio: CreateCategoryDto, UpdateCategoryDto, CategoryListQueryDto
- ai: ChatMessageDto, ChatRequestDto
- whatsapp: CreateWhatsAppConfigDto, UpdateWhatsAppConfigDto, TestConnectionDto

Validadores agregados:
- @IsNotEmpty() con mensajes descriptivos en español
- @MaxLength() en campos de texto
- @Min()/@Max() en campos numéricos y porcentajes
- @IsUUID() en campos de identificadores
- @IsEmail() en campos de email
- @IsUrl() en campos de URLs
- @IsEnum() para valores fijos
- @Matches() para patrones específicos (colores hex, códigos de moneda, slugs)
- @ArrayMaxSize() en arreglos

Build y lint exitosos.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 17:31:56 -06:00
parent c4262498ee
commit 2bbf07405b
12 changed files with 427 additions and 260 deletions

View File

@ -4,8 +4,12 @@ import {
IsArray, IsArray,
IsOptional, IsOptional,
IsNumber, IsNumber,
IsBoolean,
IsNotEmpty,
Min, Min,
Max, Max,
MaxLength,
ArrayMaxSize,
ValidateNested, ValidateNested,
IsIn, IsIn,
} from 'class-validator'; } from 'class-validator';
@ -13,50 +17,55 @@ import { Type } from 'class-transformer';
export class ChatMessageDto { export class ChatMessageDto {
@ApiProperty({ description: 'Message role', enum: ['system', 'user', 'assistant'] }) @ApiProperty({ description: 'Message role', enum: ['system', 'user', 'assistant'] })
@IsString() @IsString({ message: 'El rol debe ser texto' })
@IsIn(['system', 'user', 'assistant']) @IsIn(['system', 'user', 'assistant'], { message: 'El rol debe ser system, user o assistant' })
role: 'system' | 'user' | 'assistant'; role: 'system' | 'user' | 'assistant';
@ApiProperty({ description: 'Message content' }) @ApiProperty({ description: 'Message content' })
@IsString() @IsString({ message: 'El contenido del mensaje debe ser texto' })
@IsNotEmpty({ message: 'El contenido del mensaje es requerido' })
@MaxLength(100000, { message: 'El contenido del mensaje no debe exceder 100,000 caracteres' })
content: string; content: string;
} }
export class ChatRequestDto { export class ChatRequestDto {
@ApiProperty({ description: 'Array of chat messages', type: [ChatMessageDto] }) @ApiProperty({ description: 'Array of chat messages', type: [ChatMessageDto] })
@IsArray() @IsArray({ message: 'Los mensajes deben ser un arreglo' })
@ArrayMaxSize(100, { message: 'No se pueden enviar más de 100 mensajes a la vez' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => ChatMessageDto) @Type(() => ChatMessageDto)
messages: ChatMessageDto[]; messages: ChatMessageDto[];
@ApiPropertyOptional({ description: 'Model to use (e.g., anthropic/claude-3-haiku)' }) @ApiPropertyOptional({ description: 'Model to use (e.g., anthropic/claude-3-haiku)' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El modelo debe ser texto' })
@MaxLength(100, { message: 'El nombre del modelo no debe exceder 100 caracteres' })
model?: string; model?: string;
@ApiPropertyOptional({ description: 'Temperature (0-2)', default: 0.7 }) @ApiPropertyOptional({ description: 'Temperature (0-2)', default: 0.7 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'La temperatura debe ser un número' })
@Min(0) @Min(0, { message: 'La temperatura debe ser mayor o igual a 0' })
@Max(2) @Max(2, { message: 'La temperatura no debe exceder 2' })
temperature?: number; temperature?: number;
@ApiPropertyOptional({ description: 'Maximum tokens to generate', default: 2048 }) @ApiPropertyOptional({ description: 'Maximum tokens to generate', default: 2048 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'El máximo de tokens debe ser un número' })
@Min(1) @Min(1, { message: 'El máximo de tokens debe ser mayor o igual a 1' })
@Max(32000) @Max(32000, { message: 'El máximo de tokens no debe exceder 32,000' })
max_tokens?: number; max_tokens?: number;
@ApiPropertyOptional({ description: 'Top P sampling (0-1)', default: 1.0 }) @ApiPropertyOptional({ description: 'Top P sampling (0-1)', default: 1.0 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'Top P debe ser un número' })
@Min(0) @Min(0, { message: 'Top P debe ser mayor o igual a 0' })
@Max(1) @Max(1, { message: 'Top P no debe exceder 1' })
top_p?: number; top_p?: number;
@ApiPropertyOptional({ description: 'Stream response', default: false }) @ApiPropertyOptional({ description: 'Stream response', default: false })
@IsOptional() @IsOptional()
@IsBoolean({ message: 'El indicador de streaming debe ser verdadero o falso' })
stream?: boolean; stream?: boolean;
} }

View File

@ -1,43 +1,53 @@
import { IsEnum, IsOptional, IsString, IsBoolean } from 'class-validator'; import { IsEnum, IsOptional, IsString, IsBoolean, IsNumber, Min, Max, MaxLength, Length } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PaymentMethodType } from '../entities/payment-method.entity'; import { PaymentMethodType } from '../entities/payment-method.entity';
export class CreatePaymentMethodDto { export class CreatePaymentMethodDto {
@ApiProperty({ description: 'Payment method type', enum: PaymentMethodType }) @ApiProperty({ description: 'Payment method type', enum: PaymentMethodType })
@IsEnum(PaymentMethodType) @IsEnum(PaymentMethodType, { message: 'El tipo de método de pago debe ser válido' })
type: PaymentMethodType; type: PaymentMethodType;
@ApiPropertyOptional({ description: 'Set as default payment method' }) @ApiPropertyOptional({ description: 'Set as default payment method' })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean({ message: 'El campo por defecto debe ser verdadero o falso' })
is_default?: boolean; is_default?: boolean;
@ApiPropertyOptional({ description: 'External payment method ID from provider' }) @ApiPropertyOptional({ description: 'External payment method ID from provider' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El ID externo del método de pago debe ser texto' })
@MaxLength(255, { message: 'El ID externo no debe exceder 255 caracteres' })
external_payment_method_id?: string; external_payment_method_id?: string;
@ApiPropertyOptional({ description: 'Payment provider (stripe, conekta, etc)' }) @ApiPropertyOptional({ description: 'Payment provider (stripe, conekta, etc)' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El proveedor de pago debe ser texto' })
@MaxLength(50, { message: 'El proveedor de pago no debe exceder 50 caracteres' })
payment_provider?: string; payment_provider?: string;
// Card details (when adding a card) // Card details (when adding a card)
@ApiPropertyOptional({ description: 'Last 4 digits of card' }) @ApiPropertyOptional({ description: 'Last 4 digits of card' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'Los últimos 4 dígitos deben ser texto' })
@Length(4, 4, { message: 'Los últimos 4 dígitos de la tarjeta deben ser exactamente 4 caracteres' })
card_last_four?: string; card_last_four?: string;
@ApiPropertyOptional({ description: 'Card brand (visa, mastercard, etc)' }) @ApiPropertyOptional({ description: 'Card brand (visa, mastercard, etc)' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La marca de la tarjeta debe ser texto' })
@MaxLength(30, { message: 'La marca de la tarjeta no debe exceder 30 caracteres' })
card_brand?: string; card_brand?: string;
@ApiPropertyOptional({ description: 'Card expiry month' }) @ApiPropertyOptional({ description: 'Card expiry month' })
@IsOptional() @IsOptional()
@IsNumber({}, { message: 'El mes de expiración debe ser un número' })
@Min(1, { message: 'El mes de expiración debe ser entre 1 y 12' })
@Max(12, { message: 'El mes de expiración debe ser entre 1 y 12' })
card_exp_month?: number; card_exp_month?: number;
@ApiPropertyOptional({ description: 'Card expiry year' }) @ApiPropertyOptional({ description: 'Card expiry year' })
@IsOptional() @IsOptional()
@IsNumber({}, { message: 'El año de expiración debe ser un número' })
@Min(2024, { message: 'El año de expiración debe ser válido' })
@Max(2100, { message: 'El año de expiración debe ser válido' })
card_exp_year?: number; card_exp_year?: number;
} }

View File

@ -1,4 +1,4 @@
import { IsString, IsOptional, IsObject, IsNumber, IsEnum, IsBoolean } from 'class-validator'; import { IsString, IsOptional, IsObject, IsNumber, IsEnum, IsBoolean, IsNotEmpty, IsUrl, IsEmail, Min, Max, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum StripeWebhookEventType { export enum StripeWebhookEventType {
@ -35,104 +35,132 @@ export enum StripeWebhookEventType {
export class StripeWebhookDto { export class StripeWebhookDto {
@ApiProperty({ description: 'Webhook event ID from Stripe' }) @ApiProperty({ description: 'Webhook event ID from Stripe' })
@IsString() @IsString({ message: 'El ID del evento debe ser texto' })
@IsNotEmpty({ message: 'El ID del evento de Stripe es requerido' })
id: string; id: string;
@ApiProperty({ description: 'Event type', enum: StripeWebhookEventType }) @ApiProperty({ description: 'Event type', enum: StripeWebhookEventType })
@IsString() @IsString({ message: 'El tipo de evento debe ser texto' })
@IsNotEmpty({ message: 'El tipo de evento es requerido' })
type: string; type: string;
@ApiProperty({ description: 'Event data object' }) @ApiProperty({ description: 'Event data object' })
@IsObject() @IsObject({ message: 'Los datos del evento deben ser un objeto' })
data: { data: {
object: Record<string, any>; object: Record<string, any>;
previous_attributes?: Record<string, any>; previous_attributes?: Record<string, any>;
}; };
@ApiProperty({ description: 'API version used' }) @ApiProperty({ description: 'API version used' })
@IsString() @IsString({ message: 'La versión de API debe ser texto' })
api_version: string; api_version: string;
@ApiProperty({ description: 'Unix timestamp of event creation' }) @ApiProperty({ description: 'Unix timestamp of event creation' })
@IsNumber() @IsNumber({}, { message: 'El timestamp de creación debe ser un número' })
@Min(0, { message: 'El timestamp debe ser mayor o igual a 0' })
created: number; created: number;
@ApiPropertyOptional({ description: 'Whether this is a live mode event' }) @ApiPropertyOptional({ description: 'Whether this is a live mode event' })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean({ message: 'El modo live debe ser verdadero o falso' })
livemode?: boolean; livemode?: boolean;
} }
export class CreateStripeCustomerDto { export class CreateStripeCustomerDto {
@ApiProperty({ description: 'Tenant ID to link customer' }) @ApiProperty({ description: 'Tenant ID to link customer' })
@IsString() @IsString({ message: 'El ID del tenant debe ser texto' })
@IsNotEmpty({ message: 'El ID del tenant es requerido' })
@MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' })
tenant_id: string; tenant_id: string;
@ApiProperty({ description: 'Customer email' }) @ApiProperty({ description: 'Customer email' })
@IsString() @IsEmail({}, { message: 'El email del cliente debe tener un formato válido' })
@IsNotEmpty({ message: 'El email del cliente es requerido' })
@MaxLength(255, { message: 'El email no debe exceder 255 caracteres' })
email: string; email: string;
@ApiPropertyOptional({ description: 'Customer name' }) @ApiPropertyOptional({ description: 'Customer name' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El nombre del cliente debe ser texto' })
@MaxLength(255, { message: 'El nombre no debe exceder 255 caracteres' })
name?: string; name?: string;
@ApiPropertyOptional({ description: 'Additional metadata' }) @ApiPropertyOptional({ description: 'Additional metadata' })
@IsOptional() @IsOptional()
@IsObject() @IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, string>; metadata?: Record<string, string>;
} }
export class CreateStripeSubscriptionDto { export class CreateStripeSubscriptionDto {
@ApiProperty({ description: 'Stripe customer ID' }) @ApiProperty({ description: 'Stripe customer ID' })
@IsString() @IsString({ message: 'El ID del cliente de Stripe debe ser texto' })
@IsNotEmpty({ message: 'El ID del cliente de Stripe es requerido' })
@MaxLength(100, { message: 'El ID del cliente no debe exceder 100 caracteres' })
customer_id: string; customer_id: string;
@ApiProperty({ description: 'Stripe price ID' }) @ApiProperty({ description: 'Stripe price ID' })
@IsString() @IsString({ message: 'El ID del precio de Stripe debe ser texto' })
@IsNotEmpty({ message: 'El ID del precio de Stripe es requerido' })
@MaxLength(100, { message: 'El ID del precio no debe exceder 100 caracteres' })
price_id: string; price_id: string;
@ApiPropertyOptional({ description: 'Trial period in days' }) @ApiPropertyOptional({ description: 'Trial period in days' })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'Los días de prueba deben ser un número' })
@Min(0, { message: 'Los días de prueba deben ser mayor o igual a 0' })
@Max(365, { message: 'Los días de prueba no deben exceder 365' })
trial_period_days?: number; trial_period_days?: number;
@ApiPropertyOptional({ description: 'Additional metadata' }) @ApiPropertyOptional({ description: 'Additional metadata' })
@IsOptional() @IsOptional()
@IsObject() @IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, string>; metadata?: Record<string, string>;
} }
export class CreateCheckoutSessionDto { export class CreateCheckoutSessionDto {
@ApiProperty({ description: 'Tenant ID' }) @ApiProperty({ description: 'Tenant ID' })
@IsString() @IsString({ message: 'El ID del tenant debe ser texto' })
@IsNotEmpty({ message: 'El ID del tenant es requerido' })
@MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' })
tenant_id: string; tenant_id: string;
@ApiProperty({ description: 'Stripe price ID' }) @ApiProperty({ description: 'Stripe price ID' })
@IsString() @IsString({ message: 'El ID del precio de Stripe debe ser texto' })
@IsNotEmpty({ message: 'El ID del precio de Stripe es requerido' })
@MaxLength(100, { message: 'El ID del precio no debe exceder 100 caracteres' })
price_id: string; price_id: string;
@ApiProperty({ description: 'Success redirect URL' }) @ApiProperty({ description: 'Success redirect URL' })
@IsString() @IsUrl({}, { message: 'La URL de éxito debe ser una URL válida' })
@IsNotEmpty({ message: 'La URL de éxito es requerida' })
@MaxLength(2000, { message: 'La URL de éxito no debe exceder 2000 caracteres' })
success_url: string; success_url: string;
@ApiProperty({ description: 'Cancel redirect URL' }) @ApiProperty({ description: 'Cancel redirect URL' })
@IsString() @IsUrl({}, { message: 'La URL de cancelación debe ser una URL válida' })
@IsNotEmpty({ message: 'La URL de cancelación es requerida' })
@MaxLength(2000, { message: 'La URL de cancelación no debe exceder 2000 caracteres' })
cancel_url: string; cancel_url: string;
@ApiPropertyOptional({ description: 'Trial period in days' }) @ApiPropertyOptional({ description: 'Trial period in days' })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'Los días de prueba deben ser un número' })
@Min(0, { message: 'Los días de prueba deben ser mayor o igual a 0' })
@Max(365, { message: 'Los días de prueba no deben exceder 365' })
trial_period_days?: number; trial_period_days?: number;
} }
export class CreateBillingPortalSessionDto { export class CreateBillingPortalSessionDto {
@ApiProperty({ description: 'Tenant ID' }) @ApiProperty({ description: 'Tenant ID' })
@IsString() @IsString({ message: 'El ID del tenant debe ser texto' })
@IsNotEmpty({ message: 'El ID del tenant es requerido' })
@MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' })
tenant_id: string; tenant_id: string;
@ApiProperty({ description: 'Return URL after portal session' }) @ApiProperty({ description: 'Return URL after portal session' })
@IsString() @IsUrl({}, { message: 'La URL de retorno debe ser una URL válida' })
@IsNotEmpty({ message: 'La URL de retorno es requerida' })
@MaxLength(2000, { message: 'La URL de retorno no debe exceder 2000 caracteres' })
return_url: string; return_url: string;
} }

View File

@ -4,52 +4,53 @@ import {
IsNumber, IsNumber,
IsBoolean, IsBoolean,
IsDateString, IsDateString,
IsInt,
Min, Min,
Max, Max,
} from 'class-validator'; } from 'class-validator';
export class CreateAssignmentDto { export class CreateAssignmentDto {
@IsUUID() @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' })
userId: string; userId: string;
@IsUUID() @IsUUID('4', { message: 'El ID de esquema debe ser un UUID válido' })
schemeId: string; schemeId: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
startsAt?: string; startsAt?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
endsAt?: string; endsAt?: string;
@IsNumber() @IsNumber({}, { message: 'La tasa personalizada debe ser un número' })
@Min(0) @Min(0, { message: 'La tasa personalizada debe ser mayor o igual a 0%' })
@Max(100) @Max(100, { message: 'La tasa personalizada no debe exceder 100%' })
@IsOptional() @IsOptional()
customRate?: number; customRate?: number;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
} }
export class UpdateAssignmentDto { export class UpdateAssignmentDto {
@IsDateString() @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
startsAt?: string; startsAt?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
endsAt?: string; endsAt?: string;
@IsNumber() @IsNumber({}, { message: 'La tasa personalizada debe ser un número' })
@Min(0) @Min(0, { message: 'La tasa personalizada debe ser mayor o igual a 0%' })
@Max(100) @Max(100, { message: 'La tasa personalizada no debe exceder 100%' })
@IsOptional() @IsOptional()
customRate?: number; customRate?: number;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
} }
@ -70,21 +71,26 @@ export class AssignmentResponseDto {
} }
export class AssignmentListQueryDto { export class AssignmentListQueryDto {
@IsUUID() @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' })
@IsOptional() @IsOptional()
userId?: string; userId?: string;
@IsUUID() @IsUUID('4', { message: 'El ID de esquema debe ser un UUID válido' })
@IsOptional() @IsOptional()
schemeId?: string; schemeId?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
@IsInt({ message: 'La página debe ser un número entero' })
@Min(1, { message: 'La página debe ser mayor o igual a 1' })
@IsOptional() @IsOptional()
page?: number; page?: number;
@IsInt({ message: 'El límite debe ser un número entero' })
@Min(1, { message: 'El límite debe ser mayor o igual a 1' })
@Max(100, { message: 'El límite no debe exceder 100 elementos' })
@IsOptional() @IsOptional()
limit?: number; limit?: number;
} }

View File

@ -3,55 +3,64 @@ import {
IsOptional, IsOptional,
IsEnum, IsEnum,
IsDateString, IsDateString,
IsInt,
Min,
Max,
MaxLength, MaxLength,
IsNotEmpty,
Matches,
} from 'class-validator'; } from 'class-validator';
import { PeriodStatus } from '../entities'; import { PeriodStatus } from '../entities';
export class CreatePeriodDto { export class CreatePeriodDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @IsNotEmpty({ message: 'El nombre del período es requerido' })
@MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
name: string; name: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' })
startsAt: string; startsAt: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' })
endsAt: string; endsAt: string;
@IsString() @IsString({ message: 'La moneda debe ser texto' })
@MaxLength(3) @MaxLength(3, { message: 'El código de moneda debe ser de 3 caracteres (ej: USD, MXN)' })
@Matches(/^[A-Z]{3}$/, { message: 'El código de moneda debe ser de 3 letras mayúsculas (ej: USD, MXN)' })
@IsOptional() @IsOptional()
currency?: string; currency?: string;
} }
export class UpdatePeriodDto { export class UpdatePeriodDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
@IsOptional() @IsOptional()
name?: string; name?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
startsAt?: string; startsAt?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
endsAt?: string; endsAt?: string;
} }
export class ClosePeriodDto { export class ClosePeriodDto {
@IsString() @IsString({ message: 'Las notas deben ser texto' })
@MaxLength(1000, { message: 'Las notas no deben exceder 1000 caracteres' })
@IsOptional() @IsOptional()
notes?: string; notes?: string;
} }
export class MarkPaidDto { export class MarkPaidDto {
@IsString() @IsString({ message: 'La referencia de pago debe ser texto' })
@MaxLength(255) @MaxLength(255, { message: 'La referencia de pago no debe exceder 255 caracteres' })
@IsOptional() @IsOptional()
paymentReference?: string; paymentReference?: string;
@IsString() @IsString({ message: 'Las notas de pago deben ser texto' })
@MaxLength(1000, { message: 'Las notas de pago no deben exceder 1000 caracteres' })
@IsOptional() @IsOptional()
paymentNotes?: string; paymentNotes?: string;
} }
@ -77,13 +86,18 @@ export class PeriodResponseDto {
} }
export class PeriodListQueryDto { export class PeriodListQueryDto {
@IsEnum(PeriodStatus) @IsEnum(PeriodStatus, { message: 'El estado del período debe ser válido' })
@IsOptional() @IsOptional()
status?: PeriodStatus; status?: PeriodStatus;
@IsInt({ message: 'La página debe ser un número entero' })
@Min(1, { message: 'La página debe ser mayor o igual a 1' })
@IsOptional() @IsOptional()
page?: number; page?: number;
@IsInt({ message: 'El límite debe ser un número entero' })
@Min(1, { message: 'El límite debe ser mayor o igual a 1' })
@Max(100, { message: 'El límite no debe exceder 100 elementos' })
@IsOptional() @IsOptional()
limit?: number; limit?: number;
} }

View File

@ -1,24 +1,31 @@
import { IsString, IsEmail, IsOptional, IsArray, ValidateNested, IsObject } from 'class-validator'; import { IsString, IsEmail, IsOptional, IsArray, ValidateNested, IsObject, IsNotEmpty, MaxLength, ArrayMaxSize } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export class EmailAddressDto { export class EmailAddressDto {
@IsEmail() @IsEmail({}, { message: 'El email debe tener un formato válido' })
@IsNotEmpty({ message: 'El email es requerido' })
@MaxLength(255, { message: 'El email no debe exceder 255 caracteres' })
email: string; email: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
name?: string; name?: string;
} }
export class AttachmentDto { export class AttachmentDto {
@IsString() @IsString({ message: 'El nombre del archivo debe ser texto' })
@IsNotEmpty({ message: 'El nombre del archivo es requerido' })
@MaxLength(255, { message: 'El nombre del archivo no debe exceder 255 caracteres' })
filename: string; filename: string;
@IsString() @IsString({ message: 'El contenido debe ser texto (Base64)' })
@IsNotEmpty({ message: 'El contenido del archivo es requerido' })
content: string; // Base64 encoded content: string; // Base64 encoded
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El tipo de contenido debe ser texto' })
@MaxLength(100, { message: 'El tipo de contenido no debe exceder 100 caracteres' })
contentType?: string; contentType?: string;
} }
@ -28,36 +35,41 @@ export class SendEmailDto {
to: EmailAddressDto; to: EmailAddressDto;
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'CC debe ser un arreglo de direcciones' })
@ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en CC' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => EmailAddressDto) @Type(() => EmailAddressDto)
cc?: EmailAddressDto[]; cc?: EmailAddressDto[];
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'BCC debe ser un arreglo de direcciones' })
@ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en BCC' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => EmailAddressDto) @Type(() => EmailAddressDto)
bcc?: EmailAddressDto[]; bcc?: EmailAddressDto[];
@IsString() @IsString({ message: 'El asunto debe ser texto' })
@IsNotEmpty({ message: 'El asunto es requerido' })
@MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' })
subject: string; subject: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El texto debe ser una cadena' })
text?: string; text?: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El HTML debe ser una cadena' })
html?: string; html?: string;
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'Los adjuntos deben ser un arreglo' })
@ArrayMaxSize(10, { message: 'No se pueden incluir más de 10 archivos adjuntos' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => AttachmentDto) @Type(() => AttachmentDto)
attachments?: AttachmentDto[]; attachments?: AttachmentDto[];
@IsOptional() @IsOptional()
@IsObject() @IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
@ -67,37 +79,43 @@ export class SendTemplateEmailDto {
to: EmailAddressDto; to: EmailAddressDto;
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'CC debe ser un arreglo de direcciones' })
@ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en CC' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => EmailAddressDto) @Type(() => EmailAddressDto)
cc?: EmailAddressDto[]; cc?: EmailAddressDto[];
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'BCC debe ser un arreglo de direcciones' })
@ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en BCC' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => EmailAddressDto) @Type(() => EmailAddressDto)
bcc?: EmailAddressDto[]; bcc?: EmailAddressDto[];
@IsString() @IsString({ message: 'La clave de plantilla debe ser texto' })
@IsNotEmpty({ message: 'La clave de plantilla es requerida' })
@MaxLength(100, { message: 'La clave de plantilla no debe exceder 100 caracteres' })
templateKey: string; templateKey: string;
@IsOptional() @IsOptional()
@IsObject() @IsObject({ message: 'Las variables deben ser un objeto' })
variables?: Record<string, any>; variables?: Record<string, any>;
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'Los adjuntos deben ser un arreglo' })
@ArrayMaxSize(10, { message: 'No se pueden incluir más de 10 archivos adjuntos' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => AttachmentDto) @Type(() => AttachmentDto)
attachments?: AttachmentDto[]; attachments?: AttachmentDto[];
@IsOptional() @IsOptional()
@IsObject() @IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
export class BulkSendEmailDto { export class BulkSendEmailDto {
@IsArray() @IsArray({ message: 'Los emails deben ser un arreglo' })
@ArrayMaxSize(100, { message: 'No se pueden enviar más de 100 emails a la vez' })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SendEmailDto) @Type(() => SendEmailDto)
emails: SendEmailDto[]; emails: SendEmailDto[];

View File

@ -7,116 +7,132 @@ import {
IsObject, IsObject,
MaxLength, MaxLength,
Min, Min,
Max,
IsNotEmpty,
Matches,
IsUrl,
} from 'class-validator'; } from 'class-validator';
export class CreateCategoryDto { export class CreateCategoryDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @IsNotEmpty({ message: 'El nombre de la categoría es requerido' })
@MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
name: string; name: string;
@IsString() @IsString({ message: 'El slug debe ser texto' })
@MaxLength(120) @IsNotEmpty({ message: 'El slug de la categoría es requerido' })
@MaxLength(120, { message: 'El slug no debe exceder 120 caracteres' })
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: 'El slug solo puede contener letras minúsculas, números y guiones' })
slug: string; slug: string;
@IsUUID() @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' })
@IsOptional() @IsOptional()
parentId?: string; parentId?: string;
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' })
@IsOptional() @IsOptional()
description?: string; description?: string;
@IsInt() @IsInt({ message: 'La posición debe ser un número entero' })
@Min(0) @Min(0, { message: 'La posición debe ser mayor o igual a 0' })
@Max(1000, { message: 'La posición no debe exceder 1000' })
@IsOptional() @IsOptional()
position?: number; position?: number;
@IsString() @IsString({ message: 'La URL de imagen debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de imagen no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
imageUrl?: string; imageUrl?: string;
@IsString() @IsString({ message: 'El color debe ser texto' })
@MaxLength(7) @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' })
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' })
@IsOptional() @IsOptional()
color?: string; color?: string;
@IsString() @IsString({ message: 'El icono debe ser texto' })
@MaxLength(50) @MaxLength(50, { message: 'El icono no debe exceder 50 caracteres' })
@IsOptional() @IsOptional()
icon?: string; icon?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
@IsString() @IsString({ message: 'El meta título debe ser texto' })
@MaxLength(200) @MaxLength(200, { message: 'El meta título no debe exceder 200 caracteres' })
@IsOptional() @IsOptional()
metaTitle?: string; metaTitle?: string;
@IsString() @IsString({ message: 'La meta descripción debe ser texto' })
@MaxLength(500, { message: 'La meta descripción no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
metaDescription?: string; metaDescription?: string;
@IsObject() @IsObject({ message: 'Los campos personalizados deben ser un objeto' })
@IsOptional() @IsOptional()
customFields?: Record<string, any>; customFields?: Record<string, any>;
} }
export class UpdateCategoryDto { export class UpdateCategoryDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
@IsOptional() @IsOptional()
name?: string; name?: string;
@IsString() @IsString({ message: 'El slug debe ser texto' })
@MaxLength(120) @MaxLength(120, { message: 'El slug no debe exceder 120 caracteres' })
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: 'El slug solo puede contener letras minúsculas, números y guiones' })
@IsOptional() @IsOptional()
slug?: string; slug?: string;
@IsUUID() @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' })
@IsOptional() @IsOptional()
parentId?: string | null; parentId?: string | null;
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' })
@IsOptional() @IsOptional()
description?: string; description?: string;
@IsInt() @IsInt({ message: 'La posición debe ser un número entero' })
@Min(0) @Min(0, { message: 'La posición debe ser mayor o igual a 0' })
@Max(1000, { message: 'La posición no debe exceder 1000' })
@IsOptional() @IsOptional()
position?: number; position?: number;
@IsString() @IsString({ message: 'La URL de imagen debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de imagen no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
imageUrl?: string; imageUrl?: string;
@IsString() @IsString({ message: 'El color debe ser texto' })
@MaxLength(7) @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' })
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' })
@IsOptional() @IsOptional()
color?: string; color?: string;
@IsString() @IsString({ message: 'El icono debe ser texto' })
@MaxLength(50) @MaxLength(50, { message: 'El icono no debe exceder 50 caracteres' })
@IsOptional() @IsOptional()
icon?: string; icon?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
@IsString() @IsString({ message: 'El meta título debe ser texto' })
@MaxLength(200) @MaxLength(200, { message: 'El meta título no debe exceder 200 caracteres' })
@IsOptional() @IsOptional()
metaTitle?: string; metaTitle?: string;
@IsString() @IsString({ message: 'La meta descripción debe ser texto' })
@MaxLength(500, { message: 'La meta descripción no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
metaDescription?: string; metaDescription?: string;
@IsObject() @IsObject({ message: 'Los campos personalizados deben ser un objeto' })
@IsOptional() @IsOptional()
customFields?: Record<string, any>; customFields?: Record<string, any>;
} }
@ -149,21 +165,27 @@ export class CategoryTreeNodeDto extends CategoryResponseDto {
} }
export class CategoryListQueryDto { export class CategoryListQueryDto {
@IsUUID() @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' })
@IsOptional() @IsOptional()
parentId?: string; parentId?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
@IsString() @IsString({ message: 'El término de búsqueda debe ser texto' })
@MaxLength(100, { message: 'El término de búsqueda no debe exceder 100 caracteres' })
@IsOptional() @IsOptional()
search?: string; search?: string;
@IsInt({ message: 'La página debe ser un número entero' })
@Min(1, { message: 'La página debe ser mayor o igual a 1' })
@IsOptional() @IsOptional()
page?: number; page?: number;
@IsInt({ message: 'El límite debe ser un número entero' })
@Min(1, { message: 'El límite debe ser mayor o igual a 1' })
@Max(100, { message: 'El límite no debe exceder 100 elementos' })
@IsOptional() @IsOptional()
limit?: number; limit?: number;
} }

View File

@ -1,55 +1,63 @@
import { IsString, IsNotEmpty, IsOptional, IsArray, IsUUID } from 'class-validator'; import { IsString, IsNotEmpty, IsOptional, IsArray, IsUUID, MaxLength, Matches, ArrayMaxSize } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateRoleDto { export class CreateRoleDto {
@ApiProperty({ example: 'Manager' }) @ApiProperty({ example: 'Manager' })
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El nombre del rol es requerido' })
@MaxLength(100, { message: 'El nombre del rol no debe exceder 100 caracteres' })
name: string; name: string;
@ApiProperty({ example: 'manager' }) @ApiProperty({ example: 'manager' })
@IsString() @IsString({ message: 'El código debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El código del rol es requerido' })
@MaxLength(50, { message: 'El código del rol no debe exceder 50 caracteres' })
@Matches(/^[a-z][a-z0-9_-]*$/, { message: 'El código debe comenzar con letra minúscula y solo contener letras, números, guiones y guiones bajos' })
code: string; code: string;
@ApiPropertyOptional({ example: 'Can manage team members' }) @ApiPropertyOptional({ example: 'Can manage team members' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@MaxLength(500, { message: 'La descripción no debe exceder 500 caracteres' })
description?: string; description?: string;
@ApiPropertyOptional({ type: [String], example: ['users:read', 'users:write'] }) @ApiPropertyOptional({ type: [String], example: ['users:read', 'users:write'] })
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'Los permisos deben ser un arreglo' })
@IsString({ each: true }) @ArrayMaxSize(100, { message: 'No se pueden asignar más de 100 permisos a un rol' })
@IsString({ each: true, message: 'Cada permiso debe ser texto' })
permissions?: string[]; permissions?: string[];
} }
export class UpdateRoleDto { export class UpdateRoleDto {
@ApiPropertyOptional({ example: 'Manager' }) @ApiPropertyOptional({ example: 'Manager' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100, { message: 'El nombre del rol no debe exceder 100 caracteres' })
name?: string; name?: string;
@ApiPropertyOptional({ example: 'Can manage team members' }) @ApiPropertyOptional({ example: 'Can manage team members' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@MaxLength(500, { message: 'La descripción no debe exceder 500 caracteres' })
description?: string; description?: string;
@ApiPropertyOptional({ type: [String] }) @ApiPropertyOptional({ type: [String] })
@IsOptional() @IsOptional()
@IsArray() @IsArray({ message: 'Los permisos deben ser un arreglo' })
@IsString({ each: true }) @ArrayMaxSize(100, { message: 'No se pueden asignar más de 100 permisos a un rol' })
@IsString({ each: true, message: 'Cada permiso debe ser texto' })
permissions?: string[]; permissions?: string[];
} }
export class AssignRoleDto { export class AssignRoleDto {
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' }) @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' })
@IsUUID() @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' })
@IsNotEmpty() @IsNotEmpty({ message: 'El ID de usuario es requerido' })
userId: string; userId: string;
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440001' }) @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440001' })
@IsUUID() @IsUUID('4', { message: 'El ID de rol debe ser un UUID válido' })
@IsNotEmpty() @IsNotEmpty({ message: 'El ID de rol es requerido' })
roleId: string; roleId: string;
} }

View File

@ -8,159 +8,171 @@ import {
IsArray, IsArray,
MaxLength, MaxLength,
Min, Min,
Max,
IsBoolean, IsBoolean,
IsDateString, IsDateString,
IsNotEmpty,
IsUrl,
} from 'class-validator'; } from 'class-validator';
import { ActivityType, ActivityStatus } from '../entities'; import { ActivityType, ActivityStatus } from '../entities';
export class CreateActivityDto { export class CreateActivityDto {
@IsEnum(ActivityType) @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' })
type: ActivityType; type: ActivityType;
@IsString() @IsString({ message: 'El asunto debe ser texto' })
@MaxLength(255) @IsNotEmpty({ message: 'El asunto de la actividad es requerido' })
@MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' })
subject: string; subject: string;
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@IsOptional() @IsOptional()
@MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' })
description?: string; description?: string;
@IsUUID() @IsUUID('4', { message: 'El ID del lead debe ser un UUID válido' })
@IsOptional() @IsOptional()
leadId?: string; leadId?: string;
@IsUUID() @IsUUID('4', { message: 'El ID de la oportunidad debe ser un UUID válido' })
@IsOptional() @IsOptional()
opportunityId?: string; opportunityId?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de vencimiento debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
dueDate?: string; dueDate?: string;
@IsString() @IsString({ message: 'La hora de vencimiento debe ser texto' })
@IsOptional() @IsOptional()
@MaxLength(10, { message: 'La hora de vencimiento no debe exceder 10 caracteres' })
dueTime?: string; dueTime?: string;
@IsInt() @IsInt({ message: 'La duración debe ser un número entero' })
@Min(0) @Min(0, { message: 'La duración debe ser mayor o igual a 0 minutos' })
@Max(1440, { message: 'La duración no debe exceder 1440 minutos (24 horas)' })
@IsOptional() @IsOptional()
durationMinutes?: number; durationMinutes?: number;
@IsUUID() @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' })
@IsOptional() @IsOptional()
assignedTo?: string; assignedTo?: string;
@IsString() @IsString({ message: 'La dirección de llamada debe ser texto' })
@MaxLength(10) @MaxLength(10, { message: 'La dirección de llamada no debe exceder 10 caracteres' })
@IsOptional() @IsOptional()
callDirection?: string; callDirection?: string;
@IsString() @IsString({ message: 'La URL de grabación debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de grabación no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
callRecordingUrl?: string; callRecordingUrl?: string;
@IsString() @IsString({ message: 'La ubicación debe ser texto' })
@MaxLength(255) @MaxLength(255, { message: 'La ubicación no debe exceder 255 caracteres' })
@IsOptional() @IsOptional()
location?: string; location?: string;
@IsString() @IsString({ message: 'La URL de la reunión debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de la reunión no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
meetingUrl?: string; meetingUrl?: string;
@IsArray() @IsArray({ message: 'Los asistentes deben ser un arreglo' })
@IsOptional() @IsOptional()
attendees?: any[]; attendees?: any[];
@IsDateString() @IsDateString({}, { message: 'La fecha del recordatorio debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
reminderAt?: string; reminderAt?: string;
@IsObject() @IsObject({ message: 'Los campos personalizados deben ser un objeto' })
@IsOptional() @IsOptional()
customFields?: Record<string, any>; customFields?: Record<string, any>;
} }
export class UpdateActivityDto { export class UpdateActivityDto {
@IsEnum(ActivityType) @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' })
@IsOptional() @IsOptional()
type?: ActivityType; type?: ActivityType;
@IsEnum(ActivityStatus) @IsEnum(ActivityStatus, { message: 'El estado de la actividad debe ser válido' })
@IsOptional() @IsOptional()
status?: ActivityStatus; status?: ActivityStatus;
@IsString() @IsString({ message: 'El asunto debe ser texto' })
@MaxLength(255) @MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' })
@IsOptional() @IsOptional()
subject?: string; subject?: string;
@IsString() @IsString({ message: 'La descripción debe ser texto' })
@MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' })
@IsOptional() @IsOptional()
description?: string; description?: string;
@IsDateString() @IsDateString({}, { message: 'La fecha de vencimiento debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
dueDate?: string; dueDate?: string;
@IsString() @IsString({ message: 'La hora de vencimiento debe ser texto' })
@MaxLength(10, { message: 'La hora de vencimiento no debe exceder 10 caracteres' })
@IsOptional() @IsOptional()
dueTime?: string; dueTime?: string;
@IsInt() @IsInt({ message: 'La duración debe ser un número entero' })
@Min(0) @Min(0, { message: 'La duración debe ser mayor o igual a 0 minutos' })
@Max(1440, { message: 'La duración no debe exceder 1440 minutos (24 horas)' })
@IsOptional() @IsOptional()
durationMinutes?: number; durationMinutes?: number;
@IsString() @IsString({ message: 'El resultado debe ser texto' })
@MaxLength(500, { message: 'El resultado no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
outcome?: string; outcome?: string;
@IsUUID() @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' })
@IsOptional() @IsOptional()
assignedTo?: string; assignedTo?: string;
@IsString() @IsString({ message: 'La dirección de llamada debe ser texto' })
@MaxLength(10) @MaxLength(10, { message: 'La dirección de llamada no debe exceder 10 caracteres' })
@IsOptional() @IsOptional()
callDirection?: string; callDirection?: string;
@IsString() @IsString({ message: 'La URL de grabación debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de grabación no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
callRecordingUrl?: string; callRecordingUrl?: string;
@IsString() @IsString({ message: 'La ubicación debe ser texto' })
@MaxLength(255) @MaxLength(255, { message: 'La ubicación no debe exceder 255 caracteres' })
@IsOptional() @IsOptional()
location?: string; location?: string;
@IsString() @IsString({ message: 'La URL de la reunión debe ser texto' })
@MaxLength(500) @MaxLength(500, { message: 'La URL de la reunión no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
meetingUrl?: string; meetingUrl?: string;
@IsArray() @IsArray({ message: 'Los asistentes deben ser un arreglo' })
@IsOptional() @IsOptional()
attendees?: any[]; attendees?: any[];
@IsDateString() @IsDateString({}, { message: 'La fecha del recordatorio debe ser una fecha válida ISO 8601' })
@IsOptional() @IsOptional()
reminderAt?: string; reminderAt?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de recordatorio enviado debe ser verdadero o falso' })
@IsOptional() @IsOptional()
reminderSent?: boolean; reminderSent?: boolean;
@IsObject() @IsObject({ message: 'Los campos personalizados deben ser un objeto' })
@IsOptional() @IsOptional()
customFields?: Record<string, any>; customFields?: Record<string, any>;
} }
export class CompleteActivityDto { export class CompleteActivityDto {
@IsString() @IsString({ message: 'El resultado debe ser texto' })
@MaxLength(500, { message: 'El resultado no debe exceder 500 caracteres' })
@IsOptional() @IsOptional()
outcome?: string; outcome?: string;
} }
@ -196,29 +208,34 @@ export class ActivityResponseDto {
} }
export class ActivityListQueryDto { export class ActivityListQueryDto {
@IsEnum(ActivityType) @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' })
@IsOptional() @IsOptional()
type?: ActivityType; type?: ActivityType;
@IsEnum(ActivityStatus) @IsEnum(ActivityStatus, { message: 'El estado de la actividad debe ser válido' })
@IsOptional() @IsOptional()
status?: ActivityStatus; status?: ActivityStatus;
@IsUUID() @IsUUID('4', { message: 'El ID del lead debe ser un UUID válido' })
@IsOptional() @IsOptional()
leadId?: string; leadId?: string;
@IsUUID() @IsUUID('4', { message: 'El ID de la oportunidad debe ser un UUID válido' })
@IsOptional() @IsOptional()
opportunityId?: string; opportunityId?: string;
@IsUUID() @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' })
@IsOptional() @IsOptional()
assignedTo?: string; assignedTo?: string;
@IsInt({ message: 'La página debe ser un número entero' })
@Min(1, { message: 'La página debe ser mayor o igual a 1' })
@IsOptional() @IsOptional()
page?: number; page?: number;
@IsInt({ message: 'El límite debe ser un número entero' })
@Min(1, { message: 'El límite debe ser mayor o igual a 1' })
@Max(100, { message: 'El límite no debe exceder 100 elementos' })
@IsOptional() @IsOptional()
limit?: number; limit?: number;
} }

View File

@ -5,67 +5,80 @@ import {
IsBoolean, IsBoolean,
MaxLength, MaxLength,
Min, Min,
Max,
IsNotEmpty,
IsUUID,
IsArray,
Matches,
ArrayMaxSize,
} from 'class-validator'; } from 'class-validator';
export class CreatePipelineStageDto { export class CreatePipelineStageDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @IsNotEmpty({ message: 'El nombre de la etapa es requerido' })
@MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
name: string; name: string;
@IsInt() @IsInt({ message: 'La posición debe ser un número entero' })
@Min(0) @Min(0, { message: 'La posición debe ser mayor o igual a 0' })
@Max(100, { message: 'La posición no debe exceder 100' })
@IsOptional() @IsOptional()
position?: number; position?: number;
@IsString() @IsString({ message: 'El color debe ser texto' })
@MaxLength(7) @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' })
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' })
@IsOptional() @IsOptional()
color?: string; color?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de ganado debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isWon?: boolean; isWon?: boolean;
@IsBoolean() @IsBoolean({ message: 'El indicador de perdido debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isLost?: boolean; isLost?: boolean;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
} }
export class UpdatePipelineStageDto { export class UpdatePipelineStageDto {
@IsString() @IsString({ message: 'El nombre debe ser texto' })
@MaxLength(100) @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' })
@IsOptional() @IsOptional()
name?: string; name?: string;
@IsInt() @IsInt({ message: 'La posición debe ser un número entero' })
@Min(0) @Min(0, { message: 'La posición debe ser mayor o igual a 0' })
@Max(100, { message: 'La posición no debe exceder 100' })
@IsOptional() @IsOptional()
position?: number; position?: number;
@IsString() @IsString({ message: 'El color debe ser texto' })
@MaxLength(7) @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' })
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' })
@IsOptional() @IsOptional()
color?: string; color?: string;
@IsBoolean() @IsBoolean({ message: 'El indicador de ganado debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isWon?: boolean; isWon?: boolean;
@IsBoolean() @IsBoolean({ message: 'El indicador de perdido debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isLost?: boolean; isLost?: boolean;
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;
} }
export class ReorderStagesDto { export class ReorderStagesDto {
@IsString({ each: true }) @IsArray({ message: 'Los IDs de etapas deben ser un arreglo' })
@ArrayMaxSize(100, { message: 'No se pueden reordenar más de 100 etapas a la vez' })
@IsUUID('4', { each: true, message: 'Cada ID de etapa debe ser un UUID válido' })
stageIds: string[]; stageIds: string[];
} }

View File

@ -1,72 +1,83 @@
import { IsString, IsNumber, IsOptional, IsEnum, IsUUID, Min, Max } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsEnum, IsUUID, IsObject, IsNotEmpty, Min, Max, MaxLength } from 'class-validator';
import { FileVisibility } from '../entities/file.entity'; import { FileVisibility } from '../entities/file.entity';
// ==================== Request DTOs ==================== // ==================== Request DTOs ====================
export class GetUploadUrlDto { export class GetUploadUrlDto {
@IsString() @IsString({ message: 'El nombre del archivo debe ser texto' })
@IsNotEmpty({ message: 'El nombre del archivo es requerido' })
@MaxLength(255, { message: 'El nombre del archivo no debe exceder 255 caracteres' })
filename: string; filename: string;
@IsString() @IsString({ message: 'El tipo MIME debe ser texto' })
@IsNotEmpty({ message: 'El tipo MIME es requerido' })
@MaxLength(100, { message: 'El tipo MIME no debe exceder 100 caracteres' })
mimeType: string; mimeType: string;
@IsNumber() @IsNumber({}, { message: 'El tamaño debe ser un número' })
@Min(1) @Min(1, { message: 'El tamaño del archivo debe ser mayor a 0 bytes' })
@Max(524288000) // 500 MB max @Max(524288000, { message: 'El tamaño del archivo no debe exceder 500 MB' })
sizeBytes: number; sizeBytes: number;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La carpeta debe ser texto' })
@MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' })
folder?: string; folder?: string;
@IsOptional() @IsOptional()
@IsEnum(FileVisibility) @IsEnum(FileVisibility, { message: 'La visibilidad debe ser un valor válido' })
visibility?: FileVisibility; visibility?: FileVisibility;
} }
export class ConfirmUploadDto { export class ConfirmUploadDto {
@IsUUID() @IsUUID('4', { message: 'El ID de carga debe ser un UUID válido' })
uploadId: string; uploadId: string;
@IsOptional() @IsOptional()
@IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
export class ListFilesDto { export class ListFilesDto {
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'La página debe ser un número' })
@Min(1) @Min(1, { message: 'La página debe ser mayor o igual a 1' })
page?: number = 1; page?: number = 1;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'El límite debe ser un número' })
@Min(1) @Min(1, { message: 'El límite debe ser mayor o igual a 1' })
@Max(100) @Max(100, { message: 'El límite no debe exceder 100 elementos' })
limit?: number = 20; limit?: number = 20;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La carpeta debe ser texto' })
@MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' })
folder?: string; folder?: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El tipo MIME debe ser texto' })
@MaxLength(100, { message: 'El tipo MIME no debe exceder 100 caracteres' })
mimeType?: string; mimeType?: string;
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El término de búsqueda debe ser texto' })
@MaxLength(100, { message: 'El término de búsqueda no debe exceder 100 caracteres' })
search?: string; search?: string;
} }
export class UpdateFileDto { export class UpdateFileDto {
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'La carpeta debe ser texto' })
@MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' })
folder?: string; folder?: string;
@IsOptional() @IsOptional()
@IsEnum(FileVisibility) @IsEnum(FileVisibility, { message: 'La visibilidad debe ser un valor válido' })
visibility?: FileVisibility; visibility?: FileVisibility;
@IsOptional() @IsOptional()
@IsObject({ message: 'Los metadatos deben ser un objeto' })
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }

View File

@ -1,64 +1,74 @@
import { IsString, IsNotEmpty, IsOptional, IsBoolean, IsNumber, Min } from 'class-validator'; import { IsString, IsNotEmpty, IsOptional, IsBoolean, IsNumber, Min, Max, MaxLength, Matches } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateWhatsAppConfigDto { export class CreateWhatsAppConfigDto {
@ApiProperty({ description: 'Meta Phone Number ID' }) @ApiProperty({ description: 'Meta Phone Number ID' })
@IsString() @IsString({ message: 'El ID del número de teléfono debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El ID del número de teléfono de Meta es requerido' })
@MaxLength(50, { message: 'El ID del número de teléfono no debe exceder 50 caracteres' })
phoneNumberId: string; phoneNumberId: string;
@ApiProperty({ description: 'Meta Business Account ID' }) @ApiProperty({ description: 'Meta Business Account ID' })
@IsString() @IsString({ message: 'El ID de la cuenta de negocio debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El ID de la cuenta de negocio de Meta es requerido' })
@MaxLength(50, { message: 'El ID de la cuenta de negocio no debe exceder 50 caracteres' })
businessAccountId: string; businessAccountId: string;
@ApiProperty({ description: 'Meta Cloud API Access Token' }) @ApiProperty({ description: 'Meta Cloud API Access Token' })
@IsString() @IsString({ message: 'El token de acceso debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El token de acceso de la API de Meta es requerido' })
@MaxLength(500, { message: 'El token de acceso no debe exceder 500 caracteres' })
accessToken: string; accessToken: string;
@ApiPropertyOptional({ description: 'Webhook verify token' }) @ApiPropertyOptional({ description: 'Webhook verify token' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El token de verificación debe ser texto' })
@MaxLength(100, { message: 'El token de verificación no debe exceder 100 caracteres' })
webhookVerifyToken?: string; webhookVerifyToken?: string;
@ApiPropertyOptional({ description: 'Daily message limit', default: 1000 }) @ApiPropertyOptional({ description: 'Daily message limit', default: 1000 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'El límite diario debe ser un número' })
@Min(1) @Min(1, { message: 'El límite diario de mensajes debe ser mayor o igual a 1' })
@Max(100000, { message: 'El límite diario de mensajes no debe exceder 100,000' })
dailyMessageLimit?: number; dailyMessageLimit?: number;
} }
export class UpdateWhatsAppConfigDto { export class UpdateWhatsAppConfigDto {
@ApiPropertyOptional({ description: 'Meta Phone Number ID' }) @ApiPropertyOptional({ description: 'Meta Phone Number ID' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El ID del número de teléfono debe ser texto' })
@MaxLength(50, { message: 'El ID del número de teléfono no debe exceder 50 caracteres' })
phoneNumberId?: string; phoneNumberId?: string;
@ApiPropertyOptional({ description: 'Meta Business Account ID' }) @ApiPropertyOptional({ description: 'Meta Business Account ID' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El ID de la cuenta de negocio debe ser texto' })
@MaxLength(50, { message: 'El ID de la cuenta de negocio no debe exceder 50 caracteres' })
businessAccountId?: string; businessAccountId?: string;
@ApiPropertyOptional({ description: 'Meta Cloud API Access Token' }) @ApiPropertyOptional({ description: 'Meta Cloud API Access Token' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El token de acceso debe ser texto' })
@MaxLength(500, { message: 'El token de acceso no debe exceder 500 caracteres' })
accessToken?: string; accessToken?: string;
@ApiPropertyOptional({ description: 'Webhook verify token' }) @ApiPropertyOptional({ description: 'Webhook verify token' })
@IsOptional() @IsOptional()
@IsString() @IsString({ message: 'El token de verificación debe ser texto' })
@MaxLength(100, { message: 'El token de verificación no debe exceder 100 caracteres' })
webhookVerifyToken?: string; webhookVerifyToken?: string;
@ApiPropertyOptional({ description: 'Daily message limit' }) @ApiPropertyOptional({ description: 'Daily message limit' })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber({}, { message: 'El límite diario debe ser un número' })
@Min(1) @Min(1, { message: 'El límite diario de mensajes debe ser mayor o igual a 1' })
@Max(100000, { message: 'El límite diario de mensajes no debe exceder 100,000' })
dailyMessageLimit?: number; dailyMessageLimit?: number;
@ApiPropertyOptional({ description: 'Enable/disable WhatsApp integration' }) @ApiPropertyOptional({ description: 'Enable/disable WhatsApp integration' })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' })
isActive?: boolean; isActive?: boolean;
} }
@ -102,7 +112,8 @@ export class WhatsAppConfigResponseDto {
export class TestConnectionDto { export class TestConnectionDto {
@ApiProperty({ description: 'Phone number to send test message to' }) @ApiProperty({ description: 'Phone number to send test message to' })
@IsString() @IsString({ message: 'El número de teléfono debe ser texto' })
@IsNotEmpty() @IsNotEmpty({ message: 'El número de teléfono de prueba es requerido' })
@Matches(/^\+[1-9]\d{1,14}$/, { message: 'El número de teléfono debe estar en formato E.164 (ej: +521234567890)' })
testPhoneNumber: string; testPhoneNumber: string;
} }