import { Controller, Post, Get, Patch, Body, Param, Query, UseGuards, UseInterceptors, UploadedFiles, Request, BadRequestException, ParseIntPipe, DefaultValuePipe, Res, HttpStatus, } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiOperation, ApiResponse, ApiConsumes, ApiBody, ApiBearerAuth, ApiQuery, ApiParam, } from '@nestjs/swagger'; import { Response } from 'express'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { Public } from '../decorators/public.decorator'; import { CsvBookingService } from '../services/csv-booking.service'; import { CreateCsvBookingDto, CsvBookingResponseDto, UpdateCsvBookingStatusDto, CsvBookingListResponseDto, CsvBookingStatsDto, } from '../dto/csv-booking.dto'; /** * CSV Bookings Controller * * Handles HTTP requests for CSV-based booking requests */ @ApiTags('CSV Bookings') @Controller('csv-bookings') export class CsvBookingsController { constructor(private readonly csvBookingService: CsvBookingService) {} /** * Create a new CSV booking request * * POST /api/v1/csv-bookings */ @Post() @UseGuards(JwtAuthGuard) @ApiBearerAuth() @UseInterceptors(FilesInterceptor('documents', 10)) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Create a new CSV booking request', description: 'Creates a new booking request from CSV rate selection. Uploads documents, sends email to carrier, and creates a notification for the user.', }) @ApiBody({ schema: { type: 'object', required: [ 'carrierName', 'carrierEmail', 'origin', 'destination', 'volumeCBM', 'weightKG', 'palletCount', 'priceUSD', 'priceEUR', 'primaryCurrency', 'transitDays', 'containerType', ], properties: { carrierName: { type: 'string', example: 'SSC Consolidation' }, carrierEmail: { type: 'string', format: 'email', example: 'bookings@sscconsolidation.com' }, origin: { type: 'string', example: 'NLRTM' }, destination: { type: 'string', example: 'USNYC' }, volumeCBM: { type: 'number', example: 25.5 }, weightKG: { type: 'number', example: 3500 }, palletCount: { type: 'number', example: 10 }, priceUSD: { type: 'number', example: 1850.5 }, priceEUR: { type: 'number', example: 1665.45 }, primaryCurrency: { type: 'string', enum: ['USD', 'EUR'], example: 'USD' }, transitDays: { type: 'number', example: 28 }, containerType: { type: 'string', example: 'LCL' }, notes: { type: 'string', example: 'Handle with care' }, documents: { type: 'array', items: { type: 'string', format: 'binary' }, description: 'Shipping documents (Bill of Lading, Packing List, Invoice, etc.)', }, }, }, }) @ApiResponse({ status: 201, description: 'Booking created successfully', type: CsvBookingResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request data or missing documents' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async createBooking( @Body() dto: CreateCsvBookingDto, @UploadedFiles() files: Express.Multer.File[], @Request() req: any ): Promise { // Debug: Log request details console.log('=== CSV Booking Request Debug ==='); console.log('req.user:', req.user); console.log('req.body:', req.body); console.log('dto:', dto); console.log('files:', files?.length); console.log('================================'); if (!files || files.length === 0) { throw new BadRequestException('At least one document is required'); } // Validate user authentication if (!req.user || !req.user.id) { throw new BadRequestException('User authentication failed - no user info in request'); } if (!req.user.organizationId) { throw new BadRequestException('Organization ID is required'); } const userId = req.user.id; const organizationId = req.user.organizationId; // Convert string values to numbers (multipart/form-data sends everything as strings) const sanitizedDto: CreateCsvBookingDto = { ...dto, volumeCBM: typeof dto.volumeCBM === 'string' ? parseFloat(dto.volumeCBM) : dto.volumeCBM, weightKG: typeof dto.weightKG === 'string' ? parseFloat(dto.weightKG) : dto.weightKG, palletCount: typeof dto.palletCount === 'string' ? parseInt(dto.palletCount, 10) : dto.palletCount, priceUSD: typeof dto.priceUSD === 'string' ? parseFloat(dto.priceUSD) : dto.priceUSD, priceEUR: typeof dto.priceEUR === 'string' ? parseFloat(dto.priceEUR) : dto.priceEUR, transitDays: typeof dto.transitDays === 'string' ? parseInt(dto.transitDays, 10) : dto.transitDays, }; return await this.csvBookingService.createBooking(sanitizedDto, files, userId, organizationId); } /** * Accept a booking request (PUBLIC - token-based) * * GET /api/v1/csv-bookings/accept/:token */ @Public() @Get('accept/:token') @ApiOperation({ summary: 'Accept booking request (public)', description: 'Public endpoint for carriers to accept a booking via email link. Updates booking status and notifies the user.', }) @ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' }) @ApiResponse({ status: 200, description: 'Booking accepted successfully.', }) @ApiResponse({ status: 404, description: 'Booking not found or invalid token' }) @ApiResponse({ status: 400, description: 'Booking cannot be accepted (invalid status or expired)', }) async acceptBooking(@Param('token') token: string) { // Accept the booking const booking = await this.csvBookingService.acceptBooking(token); // Return simple success response return { success: true, bookingId: booking.id, action: 'accepted', }; } /** * Reject a booking request (PUBLIC - token-based) * * GET /api/v1/csv-bookings/reject/:token */ @Public() @Get('reject/:token') @ApiOperation({ summary: 'Reject booking request (public)', description: 'Public endpoint for carriers to reject a booking via email link. Updates booking status and notifies the user.', }) @ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' }) @ApiQuery({ name: 'reason', required: false, description: 'Rejection reason', example: 'No capacity available', }) @ApiResponse({ status: 200, description: 'Booking rejected successfully.', }) @ApiResponse({ status: 404, description: 'Booking not found or invalid token' }) @ApiResponse({ status: 400, description: 'Booking cannot be rejected (invalid status or expired)', }) async rejectBooking(@Param('token') token: string, @Query('reason') reason: string) { // Reject the booking const booking = await this.csvBookingService.rejectBooking(token, reason); // Return simple success response return { success: true, bookingId: booking.id, action: 'rejected', reason: reason || null, }; } /** * Get a booking by ID * * GET /api/v1/csv-bookings/:id */ @Get(':id') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get booking by ID', description: 'Retrieve a specific CSV booking by its ID. Only accessible by the booking owner.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Booking retrieved successfully', type: CsvBookingResponseDto, }) @ApiResponse({ status: 404, description: 'Booking not found' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getBooking(@Param('id') id: string, @Request() req: any): Promise { const userId = req.user.id; const carrierId = req.user.carrierId; // May be undefined if not a carrier return await this.csvBookingService.getBookingById(id, userId, carrierId); } /** * Get current user's bookings (paginated) * * GET /api/v1/csv-bookings */ @Get() @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get user bookings', description: 'Retrieve all bookings for the authenticated user with pagination.', }) @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 }) @ApiResponse({ status: 200, description: 'Bookings retrieved successfully', type: CsvBookingListResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getUserBookings( @Request() req: any, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number ): Promise { const userId = req.user.id; return await this.csvBookingService.getUserBookings(userId, page, limit); } /** * Get booking statistics for user * * GET /api/v1/csv-bookings/stats/me */ @Get('stats/me') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get user booking statistics', description: 'Get aggregated statistics for the authenticated user (pending, accepted, rejected, cancelled).', }) @ApiResponse({ status: 200, description: 'Statistics retrieved successfully', type: CsvBookingStatsDto, }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getUserStats(@Request() req: any): Promise { const userId = req.user.id; return await this.csvBookingService.getUserStats(userId); } /** * Cancel a booking (user action) * * PATCH /api/v1/csv-bookings/:id/cancel */ @Patch(':id/cancel') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Cancel booking', description: 'Cancel a pending booking. Only accessible by the booking owner.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Booking cancelled successfully', type: CsvBookingResponseDto, }) @ApiResponse({ status: 404, description: 'Booking not found' }) @ApiResponse({ status: 400, description: 'Booking cannot be cancelled (already accepted)' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async cancelBooking( @Param('id') id: string, @Request() req: any ): Promise { const userId = req.user.id; return await this.csvBookingService.cancelBooking(id, userId); } /** * Get organization bookings (for managers/admins) * * GET /api/v1/csv-bookings/organization/all */ @Get('organization/all') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get organization bookings', description: "Retrieve all bookings for the user's organization with pagination. For managers/admins.", }) @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 }) @ApiResponse({ status: 200, description: 'Organization bookings retrieved successfully', type: CsvBookingListResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getOrganizationBookings( @Request() req: any, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number ): Promise { const organizationId = req.user.organizationId; return await this.csvBookingService.getOrganizationBookings(organizationId, page, limit); } /** * Get organization booking statistics * * GET /api/v1/csv-bookings/stats/organization */ @Get('stats/organization') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get organization booking statistics', description: "Get aggregated statistics for the user's organization. For managers/admins.", }) @ApiResponse({ status: 200, description: 'Statistics retrieved successfully', type: CsvBookingStatsDto, }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getOrganizationStats(@Request() req: any): Promise { const organizationId = req.user.organizationId; return await this.csvBookingService.getOrganizationStats(organizationId); } }