Booking and pricing related updates

This commit is contained in:
Stephanos A
2026-06-18 14:02:11 +03:00
parent aefee5027a
commit b491f39d42
10 changed files with 410 additions and 626 deletions

View File

@@ -12,34 +12,22 @@ export class SchedulesController {
@Post('bulk-generate')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Bulk generate repetitive schedules',
description: 'Creates multiple schedules automatically by repeating every X days for the next Y days. Example: repeat every 2 days for 30 days = 15 schedules.',
})
@ApiResponse({ status: 201, description: 'Schedules generated successfully' })
@ApiResponse({ status: 400, description: 'Invalid parameters or route not found' })
@ApiOperation({ summary: 'Bulk generate repetitive schedules' })
bulkGenerateSchedules(@Body() dto: BulkCreateSchedulesDto) {
return this.service.bulkGenerateSchedules(dto);
}
@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).\nStops are automatically copied from the route's RouteStop definitions.\nYou supply the actual planned arrival/departure times per stop sequence.\nOrigin 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' })
@ApiOperation({ summary: 'Create a train schedule from a route template' })
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' })
@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,
@@ -57,57 +45,63 @@ export class SchedulesController {
@ApiResponse({ status: 201, description: 'Fare rule created' })
createFareRule(@Body() dto: CreateFareRuleDto) { return this.service.createFareRule(dto); }
@Patch('fares/:id')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-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<CreateFareRuleDto>) {
return this.service.updateFareRule(id, dto);
}
@Delete('fares/:id')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-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')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Create a segment fare rule (stop-to-stop pricing on a route)' })
@ApiResponse({ status: 201, description: 'Segment fare rule created' })
@ApiOperation({ summary: 'Create a segment fare rule' })
createSegmentFareRule(@Body() dto: any) { return this.service.createSegmentFareRule(dto); }
@Get('routes/:routeId/segment-fares')
@ApiOperation({ summary: 'List all segment fare rules for a route' })
@ApiParam({ name: 'routeId', description: 'Route UUID' })
@ApiResponse({ status: 200, description: 'List of segment fare rules' })
getSegmentFares(@Param('routeId') routeId: string) { return this.service.getSegmentFares(routeId); }
@Patch('segment-fares/:id')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update a segment fare rule' })
@ApiParam({ name: 'id', description: 'SegmentFareRule UUID' })
@ApiResponse({ status: 200, description: 'Segment fare rule updated' })
updateSegmentFareRule(@Param('id') id: string, @Body() dto: any) { return this.service.updateSegmentFareRule(id, dto); }
@Delete('segment-fares/:id')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Delete a segment fare rule' })
@ApiParam({ name: 'id', description: 'SegmentFareRule UUID' })
@ApiResponse({ status: 200, description: 'Segment fare rule deleted' })
deleteSegmentFareRule(@Param('id') id: string) { return this.service.deleteSegmentFareRule(id); }
// ===== PARAMETRIZED ROUTES (generic :id routes come AFTER specific routes) =====
@Get(':id')
@ApiOperation({ summary: 'Get schedule with train, coaches, seats, and stop timeline' })
@ApiOperation({ summary: 'Get schedule detail' })
@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')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update a schedule (partial update - times, status, coaches)' })
@ApiOperation({ summary: 'Update a schedule (partial)' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 200, description: 'Schedule updated' })
@ApiResponse({ status: 404, description: 'Schedule not found' })
updateSchedule(@Param('id') id: string, @Body() dto: UpdateScheduleDto) {
return this.service.updateSchedulePartial(id, dto);
}
@Patch(':id/status')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update schedule status (SCHEDULED → BOARDING → EN_ROUTE → ARRIVED)' })
@ApiOperation({ summary: 'Update schedule status' })
@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);
}
@@ -116,26 +110,18 @@ export class SchedulesController {
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Delete a schedule' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 200, description: 'Schedule deleted' })
@ApiResponse({ status: 404, description: 'Schedule not found' })
deleteSchedule(@Param('id') id: string) {
return this.service.deleteSchedule(id);
}
deleteSchedule(@Param('id') id: string) { return this.service.deleteSchedule(id); }
@Get(':id/stops')
@ApiOperation({ summary: 'List all stops for a schedule ordered by sequence' })
@ApiOperation({ summary: 'List all stops for a schedule' })
@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' })
@ApiOperation({ summary: 'Update a stop time' })
@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,
@@ -145,19 +131,26 @@ export class SchedulesController {
@Get(':scheduleId/fares/stored')
@ApiOperation({ summary: 'Get stored fare rules for a schedule' })
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 200, description: 'List of stored fare rules with seat class info' })
getStoredFares(@Param('scheduleId') scheduleId: string) {
return this.service.getFareRules(scheduleId);
}
@Get(':scheduleId/fares')
@ApiOperation({ summary: 'Get fare for a schedule and seat class from the fare engine' })
@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: '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' })
@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,
@@ -166,42 +159,15 @@ export class SchedulesController {
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.',
})
@ApiOperation({ summary: 'Sync fares from fare engine' })
@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);
}
syncFares(@Param('id') id: string) { return this.service.syncFaresFromEngine(id); }
@Post(':id/coaches')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Assign coaches to a schedule',
description: 'Assigns selected coaches to a schedule with their position numbers. Replaces any existing coach assignments.'
})
@ApiOperation({ summary: 'Assign coaches to a schedule' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 201, description: 'Coaches assigned successfully' })
@ApiResponse({ status: 404, description: 'Schedule or coach not found' })
assignCoaches(
@Param('id') id: string,
@Body() dto: { coaches: Array<{ coachId: string; positionNumber: number }> },
@@ -212,21 +178,14 @@ export class SchedulesController {
@Get(':id/coaches')
@ApiOperation({ summary: 'Get assigned coaches for a schedule' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
@ApiResponse({ status: 200, description: 'List of assigned coaches with seat details' })
getAssignedCoaches(@Param('id') id: string) {
return this.service.getAssignedCoaches(id);
}
getAssignedCoaches(@Param('id') id: string) { return this.service.getAssignedCoaches(id); }
@Delete(':id/coaches/:coachId')
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Remove a coach assignment from a schedule' })
@ApiOperation({ summary: 'Remove a coach assignment' })
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
@ApiParam({ name: 'coachId', description: 'Coach UUID' })
@ApiResponse({ status: 200, description: 'Coach assignment removed' })
removeCoachAssignment(
@Param('id') id: string,
@Param('coachId') coachId: string,
) {
removeCoachAssignment(@Param('id') id: string, @Param('coachId') coachId: string) {
return this.service.removeCoachAssignment(id, coachId);
}
}