347 lines
11 KiB
TypeScript
347 lines
11 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<CsvRateSearchResponseDto> {
|
|
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 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;
|
|
}
|
|
}
|
|
}
|