120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
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 { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
|
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
|
|
|
@ApiTags('Rates')
|
|
@Controller('api/v1/rates')
|
|
@ApiBearerAuth()
|
|
export class RatesController {
|
|
private readonly logger = new Logger(RatesController.name);
|
|
|
|
constructor(private readonly rateSearchService: RateSearchService) {}
|
|
|
|
@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;
|
|
}
|
|
}
|
|
}
|