xpeditis2.0/apps/backend/src/application/controllers/csv-booking-actions.controller.ts
David 8877265526 feat(blog): Phase 3 GEO/IA, corbeille, duplication, drag&drop, recadrage
- Bloc GEO/IA (resume IA, FAQ, points cles, entites) + rendu public
  "En bref"/"A retenir"/FAQ + JSON-LD Article & FAQPage.
- Corbeille: soft-delete (deleted_at), filtres Tous/Publies/Brouillons/
  Planifies/Corbeille, restauration + suppression definitive.
- Duplication d'un article (nouveau brouillon, slug unique).
- Drag & drop de l'image de couverture.
- Recadrage auto de la couverture en 16:9 (sharp) via endpoint dedie
  /admin/blog/cover-images.
- Migration AddGeoAndTrashToBlogPosts (ai_summary, faq, key_takeaways,
  ai_entities, deleted_at). Ajout dependance sharp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:44:35 +02:00

249 lines
9.1 KiB
TypeScript

import { Controller, Get, Post, Param, Query, Body, Res, StreamableFile } from '@nestjs/common';
import { Response } from 'express';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery, ApiBody } from '@nestjs/swagger';
import { Public } from '../decorators/public.decorator';
import { CsvBookingService } from '../services/csv-booking.service';
import {
CarrierDocumentsResponseDto,
VerifyDocumentAccessDto,
DocumentAccessRequirementsDto,
} from '../dto/carrier-documents.dto';
/**
* CSV Booking Actions Controller (Public Routes)
*
* Handles public accept/reject actions from carrier emails
* Separated from main controller to avoid routing conflicts
*/
@ApiTags('CSV Booking Actions')
@Controller('csv-booking-actions')
export class CsvBookingActionsController {
constructor(private readonly csvBookingService: CsvBookingService) {}
/**
* Accept a booking request (PUBLIC - token-based)
*
* GET /api/v1/csv-booking-actions/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-booking-actions/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,
};
}
/**
* Check document access requirements (PUBLIC - token-based)
*
* GET /api/v1/csv-booking-actions/documents/:token/requirements
*/
@Public()
@Get('documents/:token/requirements')
@ApiOperation({
summary: 'Check document access requirements (public)',
description:
'Check if a password is required to access booking documents. Use this before showing the password form.',
})
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
@ApiResponse({
status: 200,
description: 'Access requirements retrieved successfully.',
type: DocumentAccessRequirementsDto,
})
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
async getDocumentAccessRequirements(
@Param('token') token: string
): Promise<DocumentAccessRequirementsDto> {
return this.csvBookingService.checkDocumentAccessRequirements(token);
}
/**
* Get booking documents for carrier with password verification (PUBLIC - token-based)
*
* POST /api/v1/csv-booking-actions/documents/:token
*/
@Public()
@Post('documents/:token')
@ApiOperation({
summary: 'Get booking documents with password (public)',
description:
'Public endpoint for carriers to access booking documents after acceptance. Requires password verification. Returns booking summary and documents with signed download URLs.',
})
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
@ApiBody({ type: VerifyDocumentAccessDto })
@ApiResponse({
status: 200,
description: 'Booking documents retrieved successfully.',
type: CarrierDocumentsResponseDto,
})
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
@ApiResponse({ status: 400, description: 'Booking has not been accepted yet' })
@ApiResponse({ status: 401, description: 'Invalid password' })
async getBookingDocumentsWithPassword(
@Param('token') token: string,
@Body() dto: VerifyDocumentAccessDto
): Promise<CarrierDocumentsResponseDto> {
return this.csvBookingService.getDocumentsForCarrier(token, dto.password);
}
/**
* Get booking documents for carrier (PUBLIC - token-based) - Legacy without password
* Kept for backward compatibility with bookings created before password protection
*
* GET /api/v1/csv-booking-actions/documents/:token
*/
@Public()
@Get('documents/:token')
@ApiOperation({
summary: 'Get booking documents (public) - Legacy',
description:
'Public endpoint for carriers to access booking documents. For new bookings, use POST with password instead.',
})
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
@ApiResponse({
status: 200,
description: 'Booking documents retrieved successfully.',
type: CarrierDocumentsResponseDto,
})
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
@ApiResponse({ status: 400, description: 'Booking has not been accepted yet' })
@ApiResponse({ status: 401, description: 'Password required for this booking' })
async getBookingDocuments(@Param('token') token: string): Promise<CarrierDocumentsResponseDto> {
return this.csvBookingService.getDocumentsForCarrier(token);
}
/**
* Download a single booking document, streamed through the API (PUBLIC - token-based).
*
* Password-protected bookings must use POST with the password in the body.
*
* POST /api/v1/csv-booking-actions/documents/:token/:documentId/download
*/
@Public()
@Post('documents/:token/:documentId/download')
@ApiOperation({
summary: 'Download a booking document with password (public)',
description:
'Streams a single booking document to the carrier. Applies the same access rules as the documents list (booking accepted + password when protected).',
})
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
@ApiParam({ name: 'documentId', description: 'Document ID' })
@ApiBody({ type: VerifyDocumentAccessDto })
@ApiResponse({ status: 200, description: 'Document streamed successfully.' })
@ApiResponse({ status: 401, description: 'Invalid or missing password' })
@ApiResponse({ status: 404, description: 'Booking or document not found' })
async downloadBookingDocumentWithPassword(
@Param('token') token: string,
@Param('documentId') documentId: string,
@Body() dto: VerifyDocumentAccessDto,
@Res({ passthrough: true }) res: Response
): Promise<StreamableFile> {
return this.streamDocument(token, documentId, dto?.password, res);
}
/**
* Download a single booking document (PUBLIC - token-based) - Legacy without password.
*
* GET /api/v1/csv-booking-actions/documents/:token/:documentId/download
*/
@Public()
@Get('documents/:token/:documentId/download')
@ApiOperation({
summary: 'Download a booking document (public) - Legacy',
description:
'Streams a single booking document for bookings without password protection. Protected bookings must use the POST variant.',
})
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
@ApiParam({ name: 'documentId', description: 'Document ID' })
@ApiResponse({ status: 200, description: 'Document streamed successfully.' })
@ApiResponse({ status: 401, description: 'Password required for this booking' })
@ApiResponse({ status: 404, description: 'Booking or document not found' })
async downloadBookingDocument(
@Param('token') token: string,
@Param('documentId') documentId: string,
@Res({ passthrough: true }) res: Response
): Promise<StreamableFile> {
return this.streamDocument(token, documentId, undefined, res);
}
private async streamDocument(
token: string,
documentId: string,
password: string | undefined,
res: Response
): Promise<StreamableFile> {
const { buffer, fileName, mimeType } = await this.csvBookingService.streamDocumentForCarrier(
token,
documentId,
password
);
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
return new StreamableFile(buffer);
}
}