import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayMinSize, IsArray, IsBoolean, IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Max, Min, ValidateNested, } from 'class-validator'; /** * Per-schedule booking-window rule chosen AT CREATION, instead of inheriting the * live global rules. Mirrors {@link UpdateScheduleWindowRuleDto}, plus the * booking-close offset (which the post-creation override deliberately never * touches). Every field is optional — an omitted field falls back to the global * value, so staff can override just the one knob they care about. */ export class CreateScheduleWindowRuleDto { @ApiPropertyOptional({ example: 8, description: 'Local EAT hour the booking desk opens each day' }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) @Max(23) windowOpenHour?: number; @ApiPropertyOptional({ example: 17, description: 'Local EAT hour the booking desk shuts each day. Equal to windowOpenHour = 24-hour desk', }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) @Max(23) windowCloseHour?: number; @ApiPropertyOptional({ example: 3, description: 'How long each booking cycle stays open, in hours' }) @IsOptional() @Type(() => Number) @IsNumber() @Min(0.0166) @Max(12) windowDurationHours?: number; @ApiPropertyOptional({ example: 30, description: 'Max staff document-review minutes after the window closes' }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) docReviewMinutes?: number; @ApiPropertyOptional({ example: 60, description: 'Customer payment window minutes' }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) paymentWindowMinutes?: number; @ApiPropertyOptional({ example: 3, description: 'Days before departure the IMPORT/DOMESTIC booking window starts', }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) importWindowLeadDays?: number; @ApiPropertyOptional({ example: 24, description: 'Hours before departure the single FCFS EXPORT window opens', }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) exportBookingLeadHours?: number; @ApiPropertyOptional({ example: 180, nullable: true, description: 'Minutes before departure the booking window closes; 0/null = close at departure. ' + 'Only the offset matching the schedule direction is used (import offset for ' + 'IMPORT/DOMESTIC, export offset for EXPORT).', }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) importCloseOffsetMinutes?: number | null; @ApiPropertyOptional({ example: 1440, nullable: true, description: 'Minutes before departure an EXPORT booking window closes; 0/null = at departure', }) @IsOptional() @Type(() => Number) @IsInt() @Min(0) exportCloseOffsetMinutes?: number | null; } export class CreateContainerTrainScheduleDto { @ApiProperty({ format: 'uuid' }) @IsUUID() routeId!: string; @ApiProperty({ example: '2026-06-20T08:00:00.000Z' }) @IsDateString() scheduleDate!: string; @ApiPropertyOptional({ format: 'uuid', description: 'Built train (Train Builder) to run this departure — its locomotive set is used. Provide either trainId or locomotiveIds.', }) @IsOptional() @IsUUID() trainId?: string; @ApiPropertyOptional({ type: [String], format: 'uuid', description: 'Hand-picked locomotives pulling the train (minimum 1). Ignored when trainId is provided.', }) @IsOptional() @IsArray() @ArrayMinSize(1, { message: 'A train must be pulled by at least one locomotive' }) @IsUUID('all', { each: true }) locomotiveIds?: string[]; @ApiPropertyOptional({ description: 'Maximum total booking weight allowed on this train' }) @IsOptional() @Type(() => Number) @IsNumber() @Min(1) maxTrainWeightTons?: number; @ApiPropertyOptional({ description: 'Maximum total wagon length allowed on this train' }) @IsOptional() @Type(() => Number) @IsNumber() @Min(1) maxTrainLengthMeters?: number; @ApiPropertyOptional({ description: 'Maximum wagons allowed on this train' }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) maxWagonsPerTrain?: number; @ApiPropertyOptional({ description: 'Reverse the wagon order on this train: the physically-last wagon becomes ' + 'position 1. Frozen on the schedule; applied every time the wagon plan is ' + 'rebuilt so the stored train order and the schedule order stay in sync.', default: false, }) @IsOptional() @IsBoolean() reverseWagonOrder?: boolean; @ApiPropertyOptional({ format: 'uuid', description: 'Dedicate this departure to one shipping line. The schedule is then hidden ' + 'from every customer-facing read (windows, day pools, home cards) and shown ' + 'only to that shipping line in its portal. Omit for a normal customer train.', }) @IsOptional() @IsUUID() shippingLineCompanyId?: string; @ApiPropertyOptional({ type: CreateScheduleWindowRuleDto, description: 'Configure the booking window for THIS schedule instead of inheriting the live ' + 'global rules. Omit to use the global rules (the default). The values sent are ' + 'frozen onto the schedule as its rule snapshot, exactly as a post-creation ' + 'override would. Rejected for an IMPORT/DOMESTIC train that joins an existing ' + 'route+day group — those siblings share one window timeline, so edit the group ' + "window instead of giving one member its own.", }) @IsOptional() @ValidateNested() @Type(() => CreateScheduleWindowRuleDto) windowRule?: CreateScheduleWindowRuleDto; }