xpeditis2.0/apps/backend/src/application/controllers/bookings.controller.ts
David-Henri ARNAUD dc1c881842 feature phase 2
2025-10-09 15:03:53 +02:00

316 lines
8.9 KiB
TypeScript

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<BookingResponseDto> {
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<BookingResponseDto> {
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<BookingResponseDto> {
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<BookingListResponseDto> {
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,
};
}
}