import { Controller, Post, Get, Body, Query, HttpCode, HttpStatus, Logger, UsePipes, ValidationPipe, UseGuards, Inject, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiBearerAuth, ApiQuery, } 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, AvailableOriginsDto, AvailableDestinationsDto, RoutePortInfoDto, } from '../dto/csv-rate-upload.dto'; import { CsvRateMapper } from '../mappers/csv-rate.mapper'; import { PortRepository, PORT_REPOSITORY } from '@domain/ports/out/port.repository'; @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, @Inject(PORT_REPOSITORY) private readonly portRepository: PortRepository ) {} @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; } } /** * Search CSV-based rates with service level offers (RAPID, STANDARD, ECONOMIC) */ @Post('search-csv-offers') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Search CSV-based rates with service level offers', description: 'Search for rates from CSV-loaded carriers and generate 3 service level offers for each matching rate: RAPID (20% more expensive, 30% faster), STANDARD (base price and transit), ECONOMIC (15% cheaper, 50% slower). Results are sorted by price (cheapest first).', }) @ApiResponse({ status: HttpStatus.OK, description: 'CSV rate search with offers completed successfully', type: CsvRateSearchResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiBadRequestResponse({ description: 'Invalid request parameters', }) async searchCsvRatesWithOffers( @Body() dto: CsvRateSearchDto, @CurrentUser() user: UserPayload ): Promise { const startTime = Date.now(); this.logger.log( `[User: ${user.email}] Searching CSV rates with offers: ${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 WITH OFFERS GENERATION const result = await this.csvRateSearchService.executeWithOffers(searchInput); // Map domain output to response DTO const response = this.csvRateMapper.mapSearchOutputToResponseDto(result); const responseTimeMs = Date.now() - startTime; this.logger.log( `CSV rate search with offers completed: ${response.totalResults} results (including 3 offers per rate), ${responseTimeMs}ms` ); return response; } catch (error: any) { this.logger.error( `CSV rate search with offers failed: ${error?.message || 'Unknown error'}`, error?.stack ); throw error; } } /** * Get available origin ports from CSV rates * Returns only ports that have routes defined in CSV files */ @Get('available-routes/origins') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get available origin ports from CSV rates', description: 'Returns list of origin ports that have shipping routes defined in CSV rate files. Use this to populate origin port selection dropdown.', }) @ApiResponse({ status: HttpStatus.OK, description: 'List of available origin ports with details', type: AvailableOriginsDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) async getAvailableOrigins(): Promise { this.logger.log('Fetching available origin ports from CSV rates'); try { // Get unique origin port codes from CSV rates const originCodes = await this.csvRateSearchService.getAvailableOrigins(); // Fetch port details from database const ports = await this.portRepository.findByCodes(originCodes); // Map to response DTO with port details const origins: RoutePortInfoDto[] = originCodes.map(code => { const port = ports.find(p => p.code === code); if (port) { return { code: port.code, name: port.name, city: port.city, country: port.country, countryName: port.countryName, displayName: port.getDisplayName(), latitude: port.coordinates.latitude, longitude: port.coordinates.longitude, }; } // Fallback if port not found in database return { code, name: code, city: '', country: code.substring(0, 2), countryName: '', displayName: code, }; }); // Sort by display name origins.sort((a, b) => a.displayName.localeCompare(b.displayName)); return { origins, total: origins.length, }; } catch (error: any) { this.logger.error( `Failed to fetch available origins: ${error?.message || 'Unknown error'}`, error?.stack ); throw error; } } /** * Get available destination ports for a given origin * Returns only destinations that have routes from the specified origin in CSV files */ @Get('available-routes/destinations') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get available destination ports for a given origin', description: 'Returns list of destination ports that have shipping routes from the specified origin port in CSV rate files. Use this to populate destination port selection dropdown after origin is selected.', }) @ApiQuery({ name: 'origin', required: true, description: 'Origin port code (UN/LOCODE format, e.g., NLRTM)', example: 'NLRTM', }) @ApiResponse({ status: HttpStatus.OK, description: 'List of available destination ports with details', type: AvailableDestinationsDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiBadRequestResponse({ description: 'Origin port code is required', }) async getAvailableDestinations( @Query('origin') origin: string ): Promise { this.logger.log(`Fetching available destinations for origin: ${origin}`); if (!origin) { throw new Error('Origin port code is required'); } try { // Get destination port codes for this origin from CSV rates const destinationCodes = await this.csvRateSearchService.getAvailableDestinations(origin); // Fetch port details from database const ports = await this.portRepository.findByCodes(destinationCodes); // Map to response DTO with port details const destinations: RoutePortInfoDto[] = destinationCodes.map(code => { const port = ports.find(p => p.code === code); if (port) { return { code: port.code, name: port.name, city: port.city, country: port.country, countryName: port.countryName, displayName: port.getDisplayName(), latitude: port.coordinates.latitude, longitude: port.coordinates.longitude, }; } // Fallback if port not found in database return { code, name: code, city: '', country: code.substring(0, 2), countryName: '', displayName: code, }; }); // Sort by display name destinations.sort((a, b) => a.displayName.localeCompare(b.displayName)); return { origin: origin.toUpperCase(), destinations, total: destinations.length, }; } catch (error: any) { this.logger.error( `Failed to fetch available destinations: ${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; } } }