import { Controller, Post, Get, Patch, Delete, Body, Param, Query, UseGuards, UseInterceptors, UploadedFiles, Request, BadRequestException, ForbiddenException, ParseIntPipe, ParseUUIDPipe, DefaultValuePipe, Inject, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FilesInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiOperation, ApiResponse, ApiConsumes, ApiBody, ApiBearerAuth, ApiQuery, ApiParam, } from '@nestjs/swagger'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RolesGuard } from '../guards/roles.guard'; import { Roles } from '../decorators/roles.decorator'; import { Public } from '../decorators/public.decorator'; import { CsvBookingService } from '../services/csv-booking.service'; import { SubscriptionService } from '../services/subscription.service'; import { ShipmentCounterPort, SHIPMENT_COUNTER_PORT, } from '@domain/ports/out/shipment-counter.port'; import { OrganizationRepository, ORGANIZATION_REPOSITORY, } from '@domain/ports/out/organization.repository'; import { ShipmentLimitExceededException } from '@domain/exceptions/shipment-limit-exceeded.exception'; import { CreateCsvBookingDto, CsvBookingResponseDto, CsvBookingListResponseDto, CsvBookingStatsDto, UpdateCsvBookingDetailsDto, UpdateCsvBookingRateDto, } from '../dto/csv-booking.dto'; /** * CSV Bookings Controller * * Handles HTTP requests for CSV-based booking requests * * IMPORTANT: Route order matters in NestJS! * Static routes MUST come BEFORE parameterized routes. * Otherwise, `:id` will capture "stats", "organization", etc. */ @ApiTags('CSV Bookings') @Controller('csv-bookings') export class CsvBookingsController { constructor( private readonly csvBookingService: CsvBookingService, private readonly subscriptionService: SubscriptionService, private readonly configService: ConfigService, @Inject(SHIPMENT_COUNTER_PORT) private readonly shipmentCounter: ShipmentCounterPort, @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository ) {} // ============================================================================ // STATIC ROUTES (must come FIRST) // ============================================================================ /** * Create a new CSV booking request * * POST /api/v1/csv-bookings */ @Post() @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @ApiBearerAuth() @UseInterceptors( FilesInterceptor('documents', 10, { limits: { fileSize: 10 * 1024 * 1024, files: 10, fields: 40, parts: 50, fieldSize: 64 * 1024, }, }) ) @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 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; // ADMIN users bypass shipment limits if (req.user.role !== 'ADMIN') { // Check the paid-reservation limit (free/Bronze plan = 5 paid shipments/year) const subscription = await this.subscriptionService.getOrCreateSubscription(organizationId); const maxShipments = subscription.maxShipmentsPerYear; if (maxShipments !== -1) { const currentYear = new Date().getFullYear(); const count = await this.shipmentCounter.countPaidShipmentsForOrganizationInYear( organizationId, currentYear ); if (count >= maxShipments) { throw new ShipmentLimitExceededException(organizationId, count, maxShipments); } } } // 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); } /** * 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 the reservation (paid shipment) quota for the current organization. * * Uses the organization's real plan (not the ADMIN PLATINIUM override) and * counts only PAID shipments this year, so the UI can show an upgrade prompt. * * GET /api/v1/csv-bookings/reservation-quota */ @Get('reservation-quota') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get the paid-reservation quota for the current organization' }) @ApiResponse({ status: 200, description: 'Quota retrieved successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getReservationQuota( @Request() req: any ): Promise<{ max: number; used: number; unlimited: boolean; limitReached: boolean }> { const organizationId = req.user.organizationId; const subscription = await this.subscriptionService.getOrCreateSubscription(organizationId); const max = subscription.maxShipmentsPerYear; const unlimited = max === -1; const currentYear = new Date().getFullYear(); const used = await this.shipmentCounter.countPaidShipmentsForOrganizationInYear( organizationId, currentYear ); return { max, used, unlimited, limitReached: !unlimited && used >= max }; } /** * 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); } /** * Get organization booking statistics * * GET /api/v1/csv-bookings/stats/organization */ @Get('stats/organization') @UseGuards(RolesGuard) @Roles('ADMIN', 'MANAGER') @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); } /** * Get organization bookings (for managers/admins) * * GET /api/v1/csv-bookings/organization/all */ @Get('organization/all') @UseGuards(RolesGuard) @Roles('ADMIN', 'MANAGER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get organization bookings', description: "Retrieve all bookings and quotes of the user's organization, whoever created them, " + 'with the name of their creator. Available to every member of the organization.', }) @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); } /** * 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, }; } /** * Create Stripe Checkout session for commission payment * * POST /api/v1/csv-bookings/:id/pay */ @Post(':id/pay') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Pay commission for a booking', description: 'Creates a Stripe Checkout session for the commission payment. Returns the Stripe session URL to redirect the user to.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Stripe checkout session created', schema: { type: 'object', properties: { sessionUrl: { type: 'string' }, sessionId: { type: 'string' }, commissionAmountEur: { type: 'number' }, }, }, }) @ApiResponse({ status: 400, description: 'Booking not in QUOTE status' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async payCommission(@Param('id') id: string, @Request() req: any) { const userId = req.user.id; const userEmail = req.user.email; const organizationId = req.user.organizationId; const frontendUrl = this.configService.get('FRONTEND_URL') || 'http://localhost:3000'; // ADMIN users bypass SIRET verification if (req.user.role !== 'ADMIN') { // SIRET verification gate: organization must have a verified SIRET before paying const organization = await this.organizationRepository.findById(organizationId); if (!organization || !organization.siretVerified) { throw new ForbiddenException( 'Le numero SIRET de votre organisation doit etre verifie par un administrateur avant de pouvoir effectuer un paiement. Contactez votre administrateur.' ); } } return await this.csvBookingService.createCommissionPayment(id, userId, userEmail, frontendUrl); } /** * Confirm commission payment after Stripe redirect * * POST /api/v1/csv-bookings/:id/confirm-payment */ @Post(':id/confirm-payment') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Confirm commission payment', description: 'Called after Stripe payment success. Verifies the payment, updates booking to PENDING, sends email to carrier.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiBody({ schema: { type: 'object', required: ['sessionId'], properties: { sessionId: { type: 'string', description: 'Stripe Checkout session ID' }, }, }, }) @ApiResponse({ status: 200, description: 'Payment confirmed, booking activated', type: CsvBookingResponseDto, }) @ApiResponse({ status: 400, description: 'Payment not completed or session mismatch' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async confirmPayment( @Param('id') id: string, @Body('sessionId') sessionId: string, @Request() req: any ): Promise { if (!sessionId) { throw new BadRequestException('sessionId is required'); } const userId = req.user.id; return await this.csvBookingService.confirmCommissionPayment(id, sessionId, userId); } /** * Declare bank transfer — user confirms they have sent the wire transfer * * POST /api/v1/csv-bookings/:id/declare-transfer */ @Post(':id/declare-transfer') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Declare bank transfer', description: 'User confirms they have sent the bank wire transfer. Transitions booking to PENDING_BANK_TRANSFER awaiting admin validation.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Bank transfer declared, booking awaiting admin validation', type: CsvBookingResponseDto, }) @ApiResponse({ status: 400, description: 'Booking not in QUOTE status' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async declareTransfer( @Param('id') id: string, @Request() req: any ): Promise { const userId = req.user.id; return await this.csvBookingService.declareBankTransfer(id, userId); } // ============================================================================ // PARAMETERIZED ROUTES (must come LAST) // ============================================================================ /** * Get a booking by ID * * GET /api/v1/csv-bookings/:id * * IMPORTANT: This route MUST be after all static GET routes * Otherwise it will capture "stats", "organization", etc. */ @Get(':id') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get booking by ID', description: 'Retrieve a specific CSV booking by its ID. Readable by any member of the booking organization ' + 'and by the assigned carrier; changes remain reserved to 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, req.user.organizationId ); } /** * Cancel a booking (user action) * * PATCH /api/v1/csv-bookings/:id/cancel */ @Patch(':id/cancel') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @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); } /** * Delete an unpaid booking * * DELETE /api/v1/csv-bookings/:id */ @Delete(':id') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Delete an unpaid booking', description: 'Permanently deletes a booking whose commission has not been paid. Only accessible by the booking owner. A paid booking has been sent to the carrier and can only be cancelled.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Booking deleted successfully' }) @ApiResponse({ status: 400, description: 'Booking has been paid and cannot be deleted' }) @ApiResponse({ status: 404, description: 'Booking not found' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async deleteBooking( @Param('id', ParseUUIDPipe) id: string, @Request() req: any ): Promise<{ success: boolean; message: string }> { return await this.csvBookingService.deleteBooking(id, req.user.id); } /** * Update booking cargo details before payment * * PATCH /api/v1/csv-bookings/:id/details */ @Patch(':id/details') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Update booking details before payment', description: 'Edit cargo characteristics (volume, weight, pallets, notes) of a booking awaiting payment. Only the owner can edit, and only while the booking is QUOTE.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Booking details updated successfully', type: CsvBookingResponseDto, }) @ApiResponse({ status: 400, description: 'Booking cannot be edited (invalid status or values)' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async updateBookingDetails( @Param('id') id: string, @Body() dto: UpdateCsvBookingDetailsDto, @Request() req: any ): Promise { const userId = req.user.id; return await this.csvBookingService.updateBookingDetails(id, userId, dto); } /** * Re-apply a full rate selection before payment * * PATCH /api/v1/csv-bookings/:id/rate */ @Patch(':id/rate') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Update booking rate/route before payment', description: 'Re-apply a rate selection (carrier, route, container, transit, cargo, price) to a QUOTE booking. Only the owner can edit.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiResponse({ status: 200, description: 'Booking rate updated successfully', type: CsvBookingResponseDto, }) @ApiResponse({ status: 400, description: 'Booking cannot be edited (invalid status or values)' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async updateBookingRate( @Param('id') id: string, @Body() dto: UpdateCsvBookingRateDto, @Request() req: any ): Promise { const userId = req.user.id; return await this.csvBookingService.updateBookingRate(id, userId, dto); } /** * Add documents to an existing booking * * POST /api/v1/csv-bookings/:id/documents */ @Post(':id/documents') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @UseInterceptors( FilesInterceptor('documents', 10, { limits: { fileSize: 10 * 1024 * 1024, files: 10, fields: 40, parts: 50, fieldSize: 64 * 1024, }, }) ) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Add documents to an existing booking', description: 'Upload additional documents to a pending booking. Only the booking owner can add documents.', }) @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) @ApiBody({ schema: { type: 'object', properties: { documents: { type: 'array', items: { type: 'string', format: 'binary' }, description: 'Documents to add (max 10 files)', }, }, }, }) @ApiResponse({ status: 200, description: 'Documents added successfully', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, message: { type: 'string', example: 'Documents added successfully' }, documentsAdded: { type: 'number', example: 2 }, }, }, }) @ApiResponse({ status: 400, description: 'Invalid request or booking cannot be modified' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Booking not found' }) async addDocuments( @Param('id') id: string, @UploadedFiles() files: Express.Multer.File[], @Request() req: any ) { if (!files || files.length === 0) { throw new BadRequestException('At least one document is required'); } const userId = req.user.id; return await this.csvBookingService.addDocuments(id, files, userId); } /** * Replace a document in a booking * * PUT /api/v1/csv-bookings/:bookingId/documents/:documentId */ @Patch(':bookingId/documents/:documentId') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @UseInterceptors( FilesInterceptor('document', 1, { limits: { fileSize: 10 * 1024 * 1024, files: 1, fields: 10, parts: 11, fieldSize: 64 * 1024 }, }) ) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Replace a document in a booking', description: 'Replace an existing document with a new one. Only the booking owner can replace documents.', }) @ApiParam({ name: 'bookingId', description: 'Booking ID (UUID)' }) @ApiParam({ name: 'documentId', description: 'Document ID (UUID) to replace' }) @ApiBody({ schema: { type: 'object', properties: { document: { type: 'string', format: 'binary', description: 'New document file to replace the existing one', }, }, }, }) @ApiResponse({ status: 200, description: 'Document replaced successfully', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, message: { type: 'string', example: 'Document replaced successfully' }, newDocument: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string' }, fileName: { type: 'string' }, filePath: { type: 'string' }, mimeType: { type: 'string' }, size: { type: 'number' }, uploadedAt: { type: 'string', format: 'date-time' }, }, }, }, }, }) @ApiResponse({ status: 400, description: 'Invalid request - missing file' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Booking or document not found' }) async replaceDocument( @Param('bookingId') bookingId: string, @Param('documentId') documentId: string, @UploadedFiles() files: Express.Multer.File[], @Request() req: any ) { if (!files || files.length === 0) { throw new BadRequestException('A document file is required'); } const userId = req.user.id; return await this.csvBookingService.replaceDocument(bookingId, documentId, files[0], userId); } /** * Delete a document from a booking * * DELETE /api/v1/csv-bookings/:bookingId/documents/:documentId */ @Delete(':bookingId/documents/:documentId') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN', 'MANAGER', 'USER') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Delete a document from a booking', description: 'Remove a document from a pending booking. Only the booking owner can delete documents.', }) @ApiParam({ name: 'bookingId', description: 'Booking ID (UUID)' }) @ApiParam({ name: 'documentId', description: 'Document ID (UUID)' }) @ApiResponse({ status: 200, description: 'Document deleted successfully', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, message: { type: 'string', example: 'Document deleted successfully' }, }, }, }) @ApiResponse({ status: 400, description: 'Booking cannot be modified (not pending)' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Booking or document not found' }) async deleteDocument( @Param('bookingId') bookingId: string, @Param('documentId') documentId: string, @Request() req: any ) { const userId = req.user.id; return await this.csvBookingService.deleteDocument(bookingId, documentId, userId); } }