import { Body, Controller, Get, Param, Patch, Post, Query, ParseIntPipe, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { SchedulesService } from './schedules.service'; import { CreateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto } from './schedules.dto'; import { JwtGuard } from '../../common/jwt.guard'; import { TripStatus } from '@prisma/client'; @ApiTags('Schedule') @Controller('schedules') export class SchedulesController { constructor(private service: SchedulesService) {} @Post() @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create a train schedule from a route template', description: `Creates a schedule by referencing a Route (routeId). Stops are automatically copied from the route's RouteStop definitions. You supply the actual planned arrival/departure times per stop sequence. Origin and destination are derived from the first and last route stop — no need to specify them manually.`, }) @ApiResponse({ status: 201, description: 'Schedule created with stops copied from route template' }) @ApiResponse({ status: 400, description: 'Invalid times, inactive route, or missing planned times for some stops' }) @ApiResponse({ status: 404, description: 'Train or route not found' }) createSchedule(@Body() dto: CreateScheduleDto) { return this.service.createSchedule(dto); } @Get() @ApiOperation({ summary: 'List schedules with optional filters' }) @ApiQuery({ name: 'date', required: false, example: '2026-06-15', description: 'Departure date (YYYY-MM-DD). Returns all schedules departing on this calendar day.' }) @ApiQuery({ name: 'routeId', required: false, description: 'Filter by route UUID' }) @ApiQuery({ name: 'trainId', required: false, description: 'Filter by train UUID' }) @ApiQuery({ name: 'status', required: false, enum: TripStatus, description: 'Filter by schedule status' }) @ApiResponse({ status: 200, description: 'Array of schedules ordered by departureAt, each with train, origin/destination, stops, and booking/assignment counts' }) 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 }); } // Static routes before parameterised ones @Post('fares') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-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); } @Get(':id') @ApiOperation({ summary: 'Get schedule with train, coaches, seats, and stop timeline' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiResponse({ status: 200, description: 'Full schedule detail including route stops with station info' }) @ApiResponse({ status: 404, description: 'Schedule not found' }) getSchedule(@Param('id') id: string) { return this.service.getSchedule(id); } @Patch(':id/status') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Update schedule status (SCHEDULED → BOARDING → EN_ROUTE → ARRIVED)' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiResponse({ status: 200, description: 'Status updated' }) @ApiResponse({ status: 404, description: 'Schedule not found' }) updateStatus(@Param('id') id: string, @Body() dto: UpdateScheduleStatusDto) { return this.service.updateScheduleStatus(id, dto); } // ── Stop Times ───────────────────────────────────────────────────────────── @Get(':id/stops') @ApiOperation({ summary: 'List all stops for a schedule ordered by sequence' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiResponse({ status: 200, description: 'Ordered stop list with station details and planned/actual times' }) @ApiResponse({ status: 404, description: 'Schedule not found' }) getStops(@Param('id') id: string) { return this.service.getStops(id); } @Patch(':id/stops/:sequence') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Update planned times or live status of a specific stop' }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiParam({ name: 'sequence', description: 'Stop sequence number' }) @ApiResponse({ status: 200, description: 'Stop updated' }) @ApiResponse({ status: 404, description: 'Stop not found on schedule' }) updateStop( @Param('id') id: string, @Param('sequence', ParseIntPipe) sequence: number, @Body() dto: UpdateStopTimeDto, ) { return this.service.updateStop(id, sequence, dto); } // ── Fares ────────────────────────────────────────────────────────────────── @Get(':scheduleId/fares') @ApiOperation({ summary: 'Get fare for a schedule and seat class from the fare engine' }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'seatClassId', required: true, description: 'SeatClass UUID' }) @ApiQuery({ name: 'nationality', required: false, description: 'Passenger nationality — determines billing currency (Ethiopian→ETB, Djiboutian→DJF, other→USD)' }) @ApiResponse({ status: 200, description: 'Live fare breakdown from fare engine' }) @ApiResponse({ status: 400, description: 'Schedule has no route or missing distanceKm on stops' }) @ApiResponse({ status: 404, description: 'Schedule or seat class not found' }) getFare( @Param('scheduleId') scheduleId: string, @Query('seatClassId') seatClassId: string, @Query('nationality') nationality?: string, ) { return this.service.getFareFromEngine(scheduleId, seatClassId, nationality); } @Get(':scheduleId/fares/all') @ApiOperation({ summary: 'Get fares for all active seat classes on a schedule' }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'nationality', required: false, description: 'Passenger nationality — determines billing currency' }) @ApiResponse({ status: 200, description: 'Array of fare breakdowns for every active seat class, ordered by price ascending' }) @ApiResponse({ status: 400, description: 'Schedule has no route or missing distanceKm on stops' }) @ApiResponse({ status: 404, description: 'Schedule not found' }) getAllFares( @Param('scheduleId') scheduleId: string, @Query('nationality') nationality?: string, ) { return this.service.getAllFaresFromEngine(scheduleId, nationality); } @Post(':id/fares/sync') @ApiOperation({ summary: 'Sync fares from fare engine', description: 'Recalculates fares for all active seat classes using the fare engine (km × ratePerKm + tax) and upserts them as FareRule records scoped to this schedule. Previous active rules are expired.', }) @ApiParam({ name: 'id', description: 'TrainSchedule UUID' }) @ApiResponse({ status: 201, description: 'Fares synced — returns count of synced rules and any errors' }) @ApiResponse({ status: 400, description: 'Schedule has no associated route or missing distanceKm on stops' }) @ApiResponse({ status: 404, description: 'Schedule not found' }) syncFares(@Param('id') id: string) { return this.service.syncFaresFromEngine(id); } }