Files
edr-platform/apps/edr-passenger-api/src/modules/seats/seats.controller.ts
2026-07-01 09:03:09 +03:00

269 lines
9.6 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Patch,
Query,
SetMetadata,
UseGuards,
} from "@nestjs/common";
import {
ApiTags,
ApiOperation,
ApiBearerAuth,
ApiParam,
ApiQuery,
ApiResponse,
} from "@nestjs/swagger";
import { SeatsService } from "./seats.service";
import { HoldSeatsDto } from "./seats.dto";
import { JwtGuard } from "../../common/jwt.guard";
import { IamGuard } from "../../common/iam-adapter";
@ApiTags("Seats")
@Controller("seats")
export class SeatsController {
constructor(private service: SeatsService) {}
// ── Seat Map ──────────────────────────────────────────────────────────────
@Get("seatmap/:scheduleId")
@SetMetadata('isPublic', true)
@ApiOperation({
summary: "Get seat map filtered by coach type",
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches. Use journeyDirection to filter seat holds (OUTBOUND vs RETURN for round-trip bookings).`,
})
@ApiParam({ name: "scheduleId", description: "TrainSchedule UUID" })
@ApiQuery({
name: "coachTypeId",
required: false,
description:
"Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)",
})
@ApiQuery({
name: "journeyDirection",
required: false,
enum: ['ONE_WAY', 'OUTBOUND', 'RETURN'],
description:
"Journey direction for round-trip bookings. Filters seat holds to show only conflicting holds. Use OUTBOUND for outbound leg, RETURN for return leg. Defaults to ONE_WAY (shows all holds).",
})
@ApiQuery({
name: "originStationId",
required: false,
description: "Origin station UUID for segment-specific seat availability",
})
@ApiQuery({
name: "destinationStationId",
required: false,
description: "Destination station UUID for segment-specific seat availability",
})
@ApiResponse({
status: 200,
description:
"List of coaches of the given type with their seats and availability",
})
getSeatMap(
@Param("scheduleId") scheduleId: string,
@Query("coachTypeId") coachTypeId?: string,
@Query("journeyDirection") journeyDirection?: string,
@Query("originStationId") originStationId?: string,
@Query("destinationStationId") destinationStationId?: string,
) {
return this.service.getSeatMap(
scheduleId,
coachTypeId,
journeyDirection as any,
originStationId,
destinationStationId
);
}
// ── Hold / Release ────────────────────────────────────────────────────────
@Get("holds")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({
summary: "List active seat holds with full leg context",
description: `Returns all non-expired holds enriched with:
- **schedule**: train number, departure/arrival, full route origin→destination
- **leg**: the specific origin→destination this hold covers (station name, code, stop sequence)
- **seats**: seat label, coach, seat class, row, col
- **ttlSeconds**: seconds remaining before the hold expires
This makes it clear which segment of the route each seat is held for, enabling segment-based reuse of the same seat on non-overlapping legs.`,
})
@ApiQuery({
name: "scheduleId",
required: false,
description: "Filter by TrainSchedule UUID",
})
@ApiQuery({
name: "passengerId",
required: false,
description: "Filter by Passenger UUID",
})
@ApiResponse({
status: 200,
description: "Active holds with schedule, leg, and seat details",
})
getHolds(
@Query("scheduleId") scheduleId?: string,
@Query("passengerId") passengerId?: string,
) {
return this.service.getHolds(scheduleId, passengerId);
}
@Get("holds/:holdId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Get a single hold with full leg context" })
@ApiParam({ name: "holdId", description: "SeatHold UUID" })
@ApiResponse({
status: 200,
description: "Hold with schedule, leg, and seat details",
})
@ApiResponse({ status: 404, description: "Hold not found" })
getHold(@Param("holdId") holdId: string) {
return this.service.getHold(holdId);
}
@Post("hold")
@SetMetadata('isPublic', true)
@ApiOperation({
summary:
"Hold seats for 15 minutes before booking (Public - Guest booking supported)",
description: `Temporarily reserves seats for a passenger to complete booking.
**Features:**
- 15-minute hold duration
- Auto-release after expiry
- Prevents double booking
- Required before creating booking
- **Public endpoint** - No authentication required (supports guest booking)`,
})
@ApiResponse({
status: 201,
description: "Seats held successfully with holdId",
})
@ApiResponse({ status: 409, description: "One or more seats unavailable" })
holdSeats(@Body() dto: HoldSeatsDto) {
return this.service.holdSeats(dto);
}
@Delete("hold/:holdId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Release a seat hold" })
@ApiParam({ name: "holdId", description: "Hold UUID" })
@ApiResponse({ status: 200, description: "Hold released" })
@ApiResponse({ status: 404, description: "Hold not found" })
releaseHold(@Param("holdId") holdId: string) {
return this.service.releaseHold(holdId);
}
// ── Seat Block / Unblock ───────────────────────────────────────────────────
@Post(":seatId/block")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Block a seat (e.g., maintenance, damage)" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat blocked" })
blockSeat(@Param("seatId") seatId: string, @Body() body: { reason: string }) {
return this.service.blockSeat(seatId, body.reason);
}
@Delete(":seatId/block")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Unblock a seat" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat unblocked" })
unblockSeat(@Param("seatId") seatId: string) {
return this.service.unblockSeat(seatId);
}
// ── Maintenance ───────────────────────────────────────────────────────────
@Post(":seatId/maintenance")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Set seat status to Under Maintenance" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat set to under maintenance" })
setMaintenance(@Param("seatId") seatId: string, @Body() body: { reason: string }) {
return this.service.setMaintenance(seatId, body.reason);
}
@Delete(":seatId/maintenance")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({ summary: "Clear seat maintenance status" })
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({ status: 200, description: "Seat cleared from maintenance" })
clearMaintenance(@Param("seatId") seatId: string) {
return this.service.clearMaintenance(seatId);
}
// ── Remove Seat ────────────────────────────────────────────────────────────
@Patch(":seatId/remove")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({
summary: "Remove a seat by marking with negative seatNumber",
})
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({
status: 200,
description: "Seat removed (seatNumber negated), shows as empty space",
})
@ApiResponse({ status: 404, description: "Seat not found" })
removeSeat(@Param("seatId") seatId: string) {
return this.service.removeSeat(seatId);
}
@Patch(":seatId/undo-remove")
@UseGuards(IamGuard)
@ApiBearerAuth("IAM-auth")
@ApiOperation({
summary: "Undo seat removal by restoring original seatNumber",
})
@ApiParam({ name: "seatId", description: "Seat UUID" })
@ApiResponse({
status: 200,
description: "Seat restored (negative seatNumber removed)",
})
@ApiResponse({ status: 404, description: "Seat not found" })
@ApiResponse({ status: 400, description: "Seat is not removed" })
undoRemoveSeat(@Param("seatId") seatId: string) {
return this.service.undoRemoveSeat(seatId);
}
@Get("export/csv/:scheduleId")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Export seats as CSV" })
async exportCSV(@Param("scheduleId") scheduleId: string) {
const csv = await this.service.exportSeatsCSV(scheduleId);
return { csv, filename: `seats-${scheduleId}.csv` };
}
@Post("import/preview")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Preview CSV import" })
previewCSV(@Body() body: { csv: string }) {
return this.service.previewSeatsCSV(body.csv);
}
@Post("import/commit")
@UseGuards(JwtGuard)
@ApiBearerAuth("JWT-auth")
@ApiOperation({ summary: "Commit CSV import" })
importCSV(
@Body() body: { scheduleId: string; csv: string; commit: boolean },
) {
return this.service.importSeatsCSV(body.scheduleId, body.csv, body.commit);
}
}