xpeditis2.0/apps/backend/src/application/controllers/rates.controller.ts
2025-10-29 21:18:53 +01:00

272 lines
8.1 KiB
TypeScript

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<RateSearchResponseDto> {
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<CsvRateSearchResponseDto> {
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<AvailableCompaniesDto> {
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<FilterOptionsDto> {
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;
}
}
}