import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, ParseIntPipe } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { SchedulesService } from './schedules.service'; import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto, BulkSchedulesResponseDto, TripStatus, ApplyDelayDto } from './schedules.dto'; import { PassengerAdmin, PassengerStaff } from '../../common/passenger-guards'; import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry'; @ApiTags('Schedule') @Controller('schedules') export class SchedulesController { constructor(private service: SchedulesService) {} @Post('bulk-generate') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Bulk generate repetitive schedules' }) bulkGenerateSchedules(@Body() dto: BulkCreateSchedulesDto) { return this.service.bulkGenerateSchedules(dto); } @Post() @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Create a train schedule from a route template' }) createSchedule(@Body() dto: CreateScheduleDto) { return this.service.createSchedule(dto); } @Get() @IsPublic() @ApiOperation({ summary: 'List schedules with optional filters' }) @ApiQuery({ name: 'date', required: false }) @ApiQuery({ name: 'routeId', required: false }) @ApiQuery({ name: 'trainId', required: false }) @ApiQuery({ name: 'status', required: false, enum: TripStatus }) listSchedules( @Query('date') date?: string, @Query('routeId') routeId?: string, @Query('trainId') trainId?: string, @Query('status') status?: TripStatus, ) { return this.service.listSchedules({ date, routeId, trainId, status }); } // ===== SPECIFIC ROUTES (must come BEFORE generic :id routes) ===== @Post('fares') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Create a fare rule scoped to a schedule or route code' }) @ApiResponse({ status: 201, description: 'Fare rule created' }) createFareRule(@Body() dto: CreateFareRuleDto) { return this.service.createFareRule(dto); } @Patch('fares/:id') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update a fare rule' }) @ApiParam({ name: 'id', description: 'FareRule UUID' }) @ApiResponse({ status: 200, description: 'Fare rule updated' }) updateFareRule(@Param('id') id: string, @Body() dto: Partial) { return this.service.updateFareRule(id, dto); } @Delete('fares/:id') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Delete a fare rule' }) @ApiParam({ name: 'id', description: 'FareRule UUID' }) @ApiResponse({ status: 200, description: 'Fare rule deleted' }) deleteFareRule(@Param('id') id: string) { return this.service.deleteFareRule(id); } @Post('segment-fares') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Create a segment fare rule' }) createSegmentFareRule(@Body() dto: any) { return this.service.createSegmentFareRule(dto); } // Static sub-path MUST come before routes/:routeId/* to avoid :routeId swallowing 'fare-rules' @Delete('routes/fare-rules/:id') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Delete a route-level fare override' }) @ApiParam({ name: 'id', description: 'RouteFareRule UUID' }) deleteRouteFareRule(@Param('id') id: string) { return this.service.deleteRouteFareRule(id); } @Patch('routes/fare-rules/:id') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update a route-level fare override' }) @ApiParam({ name: 'id', description: 'RouteFareRule UUID' }) updateRouteFareRule(@Param('id') id: string, @Body() dto: any) { return this.service.updateRouteFareRule(id, dto); } @Get('routes/:routeId/fare-rules') @IsPublic() @ApiOperation({ summary: 'List route-level fare overrides for a route' }) @ApiParam({ name: 'routeId', description: 'Route UUID' }) listRouteFareRules(@Param('routeId') routeId: string) { return this.service.listRouteFareRules(routeId); } @Post('routes/:routeId/fare-rules') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Create a route-level fare override' }) @ApiParam({ name: 'routeId', description: 'Route UUID' }) createRouteFareRule(@Param('routeId') routeId: string, @Body() dto: any) { return this.service.createRouteFareRule({ ...dto, routeId }); } @Get('routes/:routeId/segment-fares') @IsPublic() @ApiOperation({ summary: 'List all segment fare rules for a route' }) @ApiParam({ name: 'routeId', description: 'Route UUID' }) getSegmentFares(@Param('routeId') routeId: string) { return this.service.getSegmentFares(routeId); } @Patch('segment-fares/:id') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update a segment fare rule' }) @ApiParam({ name: 'id', description: 'SegmentFareRule UUID' }) updateSegmentFareRule(@Param('id') id: string, @Body() dto: any) { return this.service.updateSegmentFareRule(id, dto); } @Delete('segment-fares/:id') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Delete a segment fare rule' }) @ApiParam({ name: 'id', description: 'SegmentFareRule UUID' }) deleteSegmentFareRule(@Param('id') id: string) { return this.service.deleteSegmentFareRule(id); } // ===== PARAMETRIZED ROUTES (generic :id routes come AFTER specific routes) ===== @Get(':id') @IsPublic() @ApiOperation({ summary: 'Get schedule detail' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) getSchedule(@Param('id') id: string) { return this.service.getSchedule(id); } @Patch(':id') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update a schedule (partial)' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) updateSchedule(@Param('id') id: string, @Body() dto: UpdateScheduleDto) { return this.service.updateSchedulePartial(id, dto); } @Patch(':id/status') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update schedule status' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) updateStatus(@Param('id') id: string, @Body() dto: UpdateScheduleStatusDto) { return this.service.updateScheduleStatus(id, dto); } @Delete(':id') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Delete a schedule' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'cascade', required: false, type: Boolean }) deleteSchedule(@Param('id') id: string, @Query('cascade') cascade?: string) { return this.service.deleteSchedule(id, cascade === 'true'); } @Get(':id/stops') @IsPublic() @ApiOperation({ summary: 'List all stops for a schedule' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) getStops(@Param('id') id: string) { return this.service.getStops(id); } @Patch(':id/stops/:sequence') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Update a stop time' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiParam({ name: 'sequence', description: 'Stop sequence number' }) updateStop( @Param('id') id: string, @Param('sequence', ParseIntPipe) sequence: number, @Body() dto: UpdateStopTimeDto, ) { return this.service.updateStop(id, sequence, dto); } @Post(':id/delay') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Report a delay — pushes every downstream stop\'s planned times (and check-in cutoffs) back by the same amount', description: `Shifts plannedArrivalAt/plannedDepartureAt on every stop not yet BOARDED/COMPLETED (or from fromSequence onward, if given) by delayMinutes. Since check-in cutoffs are derived directly from these planned times, this is the only action needed for booking closure to reflect the delay — no separate cutoff update. Also shifts the schedule's own departureAt/arrivalAt when the origin stop is included, and records the accumulated delay on the schedule's live status. Does not change schedule/stop status.`, }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiResponse({ status: 200, description: 'Schedule with shifted stop times' }) applyDelay(@Param('id') id: string, @Body() dto: ApplyDelayDto) { return this.service.applyDelay(id, dto); } @Put(':scheduleId/fares/:seatClassId') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Override fare for a specific seat class on a schedule', description: 'Upserts a schedule-scoped FareRule. Expires any existing active rule for the same schedule+seatClass and creates a new one.', }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiParam({ name: 'seatClassId', description: 'SeatClass UUID' }) @ApiResponse({ status: 200, description: 'Fare rule upserted' }) upsertScheduleFare( @Param('scheduleId') scheduleId: string, @Param('seatClassId') seatClassId: string, @Body() dto: { baseFareMinor: number; validFrom?: string; validUntil?: string }, ) { return this.service.upsertScheduleFare(scheduleId, seatClassId, dto); } @Get(':scheduleId/fares/stored') @ApiOperation({ summary: 'Get stored fare rules for a schedule' }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) getStoredFares(@Param('scheduleId') scheduleId: string) { return this.service.getFareRules(scheduleId); } @Get(':scheduleId/fares/all') @ApiOperation({ summary: 'Get fares for all active seat classes from the fare engine' }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'nationality', required: false }) getAllFares( @Param('scheduleId') scheduleId: string, @Query('nationality') nationality?: string, ) { return this.service.getAllFaresFromEngine(scheduleId, nationality); } @Get(':scheduleId/fares') @ApiOperation({ summary: 'Get fare for a specific seat class from the fare engine' }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'seatClassId', required: true }) @ApiQuery({ name: 'nationality', required: false }) getFare( @Param('scheduleId') scheduleId: string, @Query('seatClassId') seatClassId: string, @Query('nationality') nationality?: string, ) { return this.service.getFareFromEngine(scheduleId, seatClassId, nationality); } @Post(':id/fares/sync') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Sync fares from fare engine' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) syncFares(@Param('id') id: string) { return this.service.syncFaresFromEngine(id); } @Post(':id/coaches') @PassengerStaff([PASSENGER_PERMS.schedules.manage, PASSENGER_PERMS.admin]) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Assign coaches to a schedule' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) assignCoaches( @Param('id') id: string, @Body() dto: { coaches: Array<{ coachId: string; positionNumber: number }> }, ) { return this.service.assignCoaches(id, dto.coaches); } @Get(':id/coaches') @ApiOperation({ summary: 'Get assigned coaches for a schedule' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) getAssignedCoaches(@Param('id') id: string) { return this.service.getAssignedCoaches(id); } @Delete(':id/coaches/:coachId') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Remove a coach assignment' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiParam({ name: 'coachId', description: 'Coach UUID' }) removeCoachAssignment(@Param('id') id: string, @Param('coachId') coachId: string) { return this.service.removeCoachAssignment(id, coachId); } }