import { Controller, Post, Get, Body, HttpCode, HttpStatus, Logger, UsePipes, ValidationPipe, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiBearerAuth, } from '@nestjs/swagger'; import { RateSearchRequestDto, RateSearchResponseDto } from '../dto'; import { RateQuoteMapper } from '../mappers'; import { RateSearchService } from '@domain/services/rate-search.service'; import { CsvRateSearchService } from '@domain/services/csv-rate-search.service'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { CsvRateSearchDto, CsvRateSearchResponseDto } from '../dto/csv-rate-search.dto'; import { AvailableCompaniesDto, FilterOptionsDto } from '../dto/csv-rate-upload.dto'; import { CsvRateMapper } from '../mappers/csv-rate.mapper'; @ApiTags('Rates') @Controller('rates') @ApiBearerAuth() export class RatesController { private readonly logger = new Logger(RatesController.name); constructor( private readonly rateSearchService: RateSearchService, private readonly csvRateSearchService: CsvRateSearchService, private readonly csvRateMapper: CsvRateMapper ) {} @Post('search') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Search shipping rates', description: 'Search for available shipping rates from multiple carriers. Results are cached for 15 minutes. Requires authentication.', }) @ApiResponse({ status: HttpStatus.OK, description: 'Rate search completed successfully', type: RateSearchResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiBadRequestResponse({ description: 'Invalid request parameters', schema: { example: { statusCode: 400, message: ['Origin must be a valid 5-character UN/LOCODE (e.g., NLRTM)'], error: 'Bad Request', }, }, }) @ApiInternalServerErrorResponse({ description: 'Internal server error', }) async searchRates( @Body() dto: RateSearchRequestDto, @CurrentUser() user: UserPayload ): Promise { const startTime = Date.now(); this.logger.log( `[User: ${user.email}] Searching rates: ${dto.origin} → ${dto.destination}, ${dto.containerType}` ); try { // Convert DTO to domain input const searchInput = { origin: dto.origin, destination: dto.destination, containerType: dto.containerType, mode: dto.mode, departureDate: new Date(dto.departureDate), quantity: dto.quantity, weight: dto.weight, volume: dto.volume, isHazmat: dto.isHazmat, imoClass: dto.imoClass, }; // Execute search const result = await this.rateSearchService.execute(searchInput); // Convert domain entities to DTOs const quoteDtos = RateQuoteMapper.toDtoArray(result.quotes); const responseTimeMs = Date.now() - startTime; this.logger.log(`Rate search completed: ${quoteDtos.length} quotes, ${responseTimeMs}ms`); return { quotes: quoteDtos, count: quoteDtos.length, origin: dto.origin, destination: dto.destination, departureDate: dto.departureDate, containerType: dto.containerType, mode: dto.mode, fromCache: false, // TODO: Implement cache detection responseTimeMs, }; } catch (error: any) { this.logger.error(`Rate search failed: ${error?.message || 'Unknown error'}`, error?.stack); throw error; } } /** * Search CSV-based rates with advanced filters */ @Post('search-csv') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Search CSV-based rates with advanced filters', description: 'Search for rates from CSV-loaded carriers (SSC, ECU, TCC, NVO) with advanced filtering options including volume, weight, pallets, price range, transit time, and more.', }) @ApiResponse({ status: HttpStatus.OK, description: 'CSV rate search completed successfully', type: CsvRateSearchResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiBadRequestResponse({ description: 'Invalid request parameters', }) async searchCsvRates( @Body() dto: CsvRateSearchDto, @CurrentUser() user: UserPayload ): Promise { const startTime = Date.now(); this.logger.log( `[User: ${user.email}] Searching CSV rates: ${dto.origin} → ${dto.destination}, ${dto.volumeCBM} CBM, ${dto.weightKG} kg` ); try { // Map DTO to domain input const searchInput = { origin: dto.origin, destination: dto.destination, volumeCBM: dto.volumeCBM, weightKG: dto.weightKG, palletCount: dto.palletCount ?? 0, containerType: dto.containerType, filters: this.csvRateMapper.mapFiltersDtoToDomain(dto.filters), // Service requirements for detailed pricing hasDangerousGoods: dto.hasDangerousGoods ?? false, requiresSpecialHandling: dto.requiresSpecialHandling ?? false, requiresTailgate: dto.requiresTailgate ?? false, requiresStraps: dto.requiresStraps ?? false, requiresThermalCover: dto.requiresThermalCover ?? false, hasRegulatedProducts: dto.hasRegulatedProducts ?? false, requiresAppointment: dto.requiresAppointment ?? false, }; // Execute CSV rate search const result = await this.csvRateSearchService.execute(searchInput); // Map domain output to response DTO const response = this.csvRateMapper.mapSearchOutputToResponseDto(result); const responseTimeMs = Date.now() - startTime; this.logger.log( `CSV rate search completed: ${response.totalResults} results, ${responseTimeMs}ms` ); return response; } catch (error: any) { this.logger.error( `CSV rate search failed: ${error?.message || 'Unknown error'}`, error?.stack ); throw error; } } /** * Get available companies */ @Get('companies') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get available carrier companies', description: 'Returns list of all available carrier companies in the CSV rate system.', }) @ApiResponse({ status: HttpStatus.OK, description: 'List of available companies', type: AvailableCompaniesDto, }) async getCompanies(): Promise { this.logger.log('Fetching available companies'); try { const companies = await this.csvRateSearchService.getAvailableCompanies(); return { companies, total: companies.length, }; } catch (error: any) { this.logger.error( `Failed to fetch companies: ${error?.message || 'Unknown error'}`, error?.stack ); throw error; } } /** * Get filter options */ @Get('filters/options') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get available filter options', description: 'Returns available options for all filters (companies, container types, currencies).', }) @ApiResponse({ status: HttpStatus.OK, description: 'Available filter options', type: FilterOptionsDto, }) async getFilterOptions(): Promise { this.logger.log('Fetching filter options'); try { const [companies, containerTypes] = await Promise.all([ this.csvRateSearchService.getAvailableCompanies(), this.csvRateSearchService.getAvailableContainerTypes(), ]); return { companies, containerTypes, currencies: ['USD', 'EUR'], }; } catch (error: any) { this.logger.error( `Failed to fetch filter options: ${error?.message || 'Unknown error'}`, error?.stack ); throw error; } } }