import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEmail, IsEnum, IsInt, IsOptional, IsString, Length, Max, MaxLength, Min, MinLength, } from 'class-validator'; export enum SupportStatusDto { OPEN = 'OPEN', RESOLVED = 'RESOLVED', CLOSED = 'CLOSED', } export class CreateConversationDto { @ApiProperty({ description: 'Short subject / topic of the request.' }) @IsString() @Length(3, 200) subject!: string; @ApiProperty({ description: 'The first message body.' }) @IsString() @MinLength(1) @MaxLength(4000) initialMessage!: string; } /** * Text is optional across the send DTOs because a message may be nothing but * attachments. "Neither text nor files" is rejected in the service rather than * here — the validator can't see the multipart file parts. */ export class SendMessageDto { @ApiPropertyOptional({ description: 'Message text. Optional only when attachments are present.', }) @IsOptional() @IsString() @MaxLength(4000) text?: string; } export class CreateGuestConversationDto { @ApiProperty({ description: 'Client-generated anonymous id (localStorage).' }) @IsString() @Length(8, 120) guestId!: string; @ApiProperty({ description: 'Guest full name.' }) @IsString() @Length(1, 120) name!: string; @ApiProperty({ description: 'Guest email for follow-up.' }) @IsEmail() email!: string; @ApiPropertyOptional({ description: 'Guest phone (optional).' }) @IsOptional() @IsString() @MaxLength(40) phone?: string; @ApiProperty() @IsString() @Length(3, 200) subject!: string; @ApiProperty() @IsString() @MinLength(1) @MaxLength(4000) initialMessage!: string; } export class GuestSendMessageDto { @ApiProperty({ description: 'The guest id that owns the conversation.' }) @IsString() @Length(8, 120) guestId!: string; @ApiPropertyOptional({ description: 'Message text. Optional only when attachments are present.', }) @IsOptional() @IsString() @MaxLength(4000) text?: string; } export class GuestIdBodyDto { @ApiProperty() @IsString() @Length(8, 120) guestId!: string; } export class DeviceSendMessageDto { @ApiProperty({ description: 'Client device id (localStorage).' }) @IsString() @Length(8, 120) deviceId!: string; @ApiPropertyOptional({ description: 'Message text. Optional only when attachments are present.', }) @IsOptional() @IsString() @MaxLength(4000) text?: string; } export class DeviceIdBodyDto { @ApiProperty() @IsString() @Length(8, 120) deviceId!: string; } export class UpdateStatusDto { @ApiProperty({ enum: SupportStatusDto }) @IsEnum(SupportStatusDto) status!: SupportStatusDto; } export class ListConversationsQueryDto { @ApiPropertyOptional({ enum: SupportStatusDto }) @IsOptional() @IsEnum(SupportStatusDto) status?: SupportStatusDto; @ApiPropertyOptional({ description: 'Search subject / passenger name.' }) @IsOptional() @IsString() search?: string; @ApiPropertyOptional({ minimum: 1, default: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number; @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 100 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number; } /** Default page size for a thread — roughly two screens of bubbles. */ export const SUPPORT_MESSAGES_DEFAULT_LIMIT = 30; export const SUPPORT_MESSAGES_MAX_LIMIT = 100; export class ListMessagesQueryDto { @ApiPropertyOptional({ description: "Opaque cursor from a previous response's `nextCursor`. Returns the page " + 'of messages immediately OLDER than the cursor. Omit for the newest page.', }) @IsOptional() @IsString() before?: string; @ApiPropertyOptional({ minimum: 1, maximum: SUPPORT_MESSAGES_MAX_LIMIT, default: SUPPORT_MESSAGES_DEFAULT_LIMIT, }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(SUPPORT_MESSAGES_MAX_LIMIT) limit?: number; } /** Query form of {@link ListMessagesQueryDto} for the device-scoped thread. */ export class DeviceThreadQueryDto extends ListMessagesQueryDto { @ApiProperty({ description: 'Client device id (localStorage).' }) @IsString() @Length(8, 120) deviceId!: string; }