import { Controller, Get, Post, Param, Body, Query, HttpCode, HttpStatus, Logger, UsePipes, ValidationPipe, NotFoundException, ParseUUIDPipe, ParseIntPipe, DefaultValuePipe, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBadRequestResponse, ApiNotFoundResponse, ApiInternalServerErrorResponse, ApiQuery, ApiParam, ApiBearerAuth, } from '@nestjs/swagger'; import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto, } from '../dto'; import { BookingMapper } from '../mappers'; import { BookingService } from '../../domain/services/booking.service'; import { BookingRepository } from '../../domain/ports/out/booking.repository'; import { RateQuoteRepository } from '../../domain/ports/out/rate-quote.repository'; import { BookingNumber } from '../../domain/value-objects/booking-number.vo'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; @ApiTags('Bookings') @Controller('api/v1/bookings') @UseGuards(JwtAuthGuard) @ApiBearerAuth() export class BookingsController { private readonly logger = new Logger(BookingsController.name); constructor( private readonly bookingService: BookingService, private readonly bookingRepository: BookingRepository, private readonly rateQuoteRepository: RateQuoteRepository, ) {} @Post() @HttpCode(HttpStatus.CREATED) @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Create a new booking', description: 'Create a new booking based on a rate quote. The booking will be in "draft" status initially. Requires authentication.', }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Booking created successfully', type: BookingResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiBadRequestResponse({ description: 'Invalid request parameters', }) @ApiNotFoundResponse({ description: 'Rate quote not found', }) @ApiInternalServerErrorResponse({ description: 'Internal server error', }) async createBooking( @Body() dto: CreateBookingRequestDto, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[User: ${user.email}] Creating booking for rate quote: ${dto.rateQuoteId}`, ); try { // Convert DTO to domain input, using authenticated user's data const input = { ...BookingMapper.toCreateBookingInput(dto), userId: user.id, organizationId: user.organizationId, }; // Create booking via domain service const booking = await this.bookingService.createBooking(input); // Fetch rate quote for response const rateQuote = await this.rateQuoteRepository.findById(dto.rateQuoteId); if (!rateQuote) { throw new NotFoundException(`Rate quote ${dto.rateQuoteId} not found`); } // Convert to DTO const response = BookingMapper.toDto(booking, rateQuote); this.logger.log( `Booking created successfully: ${booking.bookingNumber.value} (${booking.id})`, ); return response; } catch (error: any) { this.logger.error( `Booking creation failed: ${error?.message || 'Unknown error'}`, error?.stack, ); throw error; } } @Get(':id') @ApiOperation({ summary: 'Get booking by ID', description: 'Retrieve detailed information about a specific booking. Requires authentication.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)', example: '550e8400-e29b-41d4-a716-446655440000', }) @ApiResponse({ status: HttpStatus.OK, description: 'Booking details retrieved successfully', type: BookingResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiNotFoundResponse({ description: 'Booking not found', }) async getBooking( @Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload, ): Promise { this.logger.log(`[User: ${user.email}] Fetching booking: ${id}`); const booking = await this.bookingRepository.findById(id); if (!booking) { throw new NotFoundException(`Booking ${id} not found`); } // Verify booking belongs to user's organization if (booking.organizationId !== user.organizationId) { throw new NotFoundException(`Booking ${id} not found`); } // Fetch rate quote const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); if (!rateQuote) { throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`); } return BookingMapper.toDto(booking, rateQuote); } @Get('number/:bookingNumber') @ApiOperation({ summary: 'Get booking by booking number', description: 'Retrieve detailed information about a specific booking using its booking number. Requires authentication.', }) @ApiParam({ name: 'bookingNumber', description: 'Booking number', example: 'WCM-2025-ABC123', }) @ApiResponse({ status: HttpStatus.OK, description: 'Booking details retrieved successfully', type: BookingResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiNotFoundResponse({ description: 'Booking not found', }) async getBookingByNumber( @Param('bookingNumber') bookingNumber: string, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[User: ${user.email}] Fetching booking by number: ${bookingNumber}`, ); const bookingNumberVo = BookingNumber.fromString(bookingNumber); const booking = await this.bookingRepository.findByBookingNumber(bookingNumberVo); if (!booking) { throw new NotFoundException(`Booking ${bookingNumber} not found`); } // Verify booking belongs to user's organization if (booking.organizationId !== user.organizationId) { throw new NotFoundException(`Booking ${bookingNumber} not found`); } // Fetch rate quote const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); if (!rateQuote) { throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`); } return BookingMapper.toDto(booking, rateQuote); } @Get() @ApiOperation({ summary: 'List bookings', description: "Retrieve a paginated list of bookings for the authenticated user's organization. Requires authentication.", }) @ApiQuery({ name: 'page', required: false, description: 'Page number (1-based)', example: 1, }) @ApiQuery({ name: 'pageSize', required: false, description: 'Number of items per page', example: 20, }) @ApiQuery({ name: 'status', required: false, description: 'Filter by booking status', enum: [ 'draft', 'pending_confirmation', 'confirmed', 'in_transit', 'delivered', 'cancelled', ], }) @ApiResponse({ status: HttpStatus.OK, description: 'Bookings list retrieved successfully', type: BookingListResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) async listBookings( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number, @Query('status') status: string | undefined, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[User: ${user.email}] Listing bookings: page=${page}, pageSize=${pageSize}, status=${status}`, ); // Use authenticated user's organization ID const organizationId = user.organizationId; // Fetch bookings for the user's organization const bookings = await this.bookingRepository.findByOrganization(organizationId); // Filter by status if provided const filteredBookings = status ? bookings.filter((b: any) => b.status.value === status) : bookings; // Paginate const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedBookings = filteredBookings.slice(startIndex, endIndex); // Fetch rate quotes for all bookings const bookingsWithQuotes = await Promise.all( paginatedBookings.map(async (booking: any) => { const rateQuote = await this.rateQuoteRepository.findById( booking.rateQuoteId, ); return { booking, rateQuote: rateQuote! }; }), ); // Convert to DTOs const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes); const totalPages = Math.ceil(filteredBookings.length / pageSize); return { bookings: bookingDtos, total: filteredBookings.length, page, pageSize, totalPages, }; } }