import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { type CommandBus, type QueryBus } from '@nestjs/cqrs'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard, CurrentUser } from '@modules/auth'; import { EndpointRateLimit, EndpointRateLimitGuard, NotFoundException } from '@modules/shared'; import { CreateTransferListingCommand } from '../../application/commands/create-transfer-listing/create-transfer-listing.command'; import { EstimateFromPhotosCommand } from '../../application/commands/estimate-from-photos/estimate-from-photos.command'; import { EstimateTransferPricesCommand } from '../../application/commands/estimate-transfer-prices/estimate-transfer-prices.command'; import { UpdateTransferListingCommand } from '../../application/commands/update-transfer-listing/update-transfer-listing.command'; import { GetTransferListingQuery } from '../../application/queries/get-transfer-listing/get-transfer-listing.query'; import { ListTransferListingsQuery } from '../../application/queries/list-transfer-listings/list-transfer-listings.query'; import { TransferStatsQuery } from '../../application/queries/transfer-stats/transfer-stats.query'; import { type CreateTransferListingDto } from '../dto/create-transfer-listing.dto'; import { type EstimateFromPhotosDto } from '../dto/estimate-from-photos.dto'; import { type EstimateTransferPricesDto } from '../dto/estimate-transfer-prices.dto'; import { type SearchTransferListingsDto } from '../dto/search-transfer-listings.dto'; import { type UpdateTransferListingDto } from '../dto/update-transfer-listing.dto'; @ApiTags('transfer') @Controller('transfer') export class TransferController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} // ── Public endpoints ────────────────────────────────────────────── @ApiOperation({ summary: 'Danh sách sang nhượng', description: 'Tìm kiếm và lọc tin sang nhượng' }) @ApiResponse({ status: 200, description: 'Danh sách tin sang nhượng phân trang' }) @Get('listings') async listListings(@Query() dto: SearchTransferListingsDto) { return this.queryBus.execute( new ListTransferListingsQuery( dto.q, dto.category, dto.status, dto.district, dto.city, dto.minPrice, dto.maxPrice, undefined, // sellerId — only used for "my listings" queries dto.page ?? 1, dto.limit ?? 20, ), ); } @ApiOperation({ summary: 'Chi tiết tin sang nhượng', description: 'Xem chi tiết tin sang nhượng theo ID' }) @ApiResponse({ status: 200, description: 'Thông tin chi tiết tin sang nhượng' }) @ApiResponse({ status: 404, description: 'Không tìm thấy tin sang nhượng' }) @Get('listings/:id') async getListing(@Param('id') id: string) { const result = await this.queryBus.execute(new GetTransferListingQuery(id)); if (!result) { throw new NotFoundException('Transfer listing', id); } return result; } @ApiOperation({ summary: 'Thống kê sang nhượng', description: 'Tổng quan thống kê tin sang nhượng' }) @ApiResponse({ status: 200, description: 'Dữ liệu thống kê' }) @Get('stats') async getStats() { return this.queryBus.execute(new TransferStatsQuery()); } @ApiOperation({ summary: 'Ước tính giá AI', description: 'Ước tính giá trị nội thất/thiết bị dựa trên khấu hao, thương hiệu và tình trạng' }) @ApiResponse({ status: 200, description: 'Kết quả ước tính giá' }) @Post('estimate') async estimatePrices(@Body() dto: EstimateTransferPricesDto) { return this.commandBus.execute( new EstimateTransferPricesCommand(dto.items), ); } @ApiOperation({ summary: 'Ước tính giá từ ảnh (AI Vision)', description: 'Sử dụng Claude Vision để đánh giá tình trạng nội thất từ ảnh, kết hợp với công cụ định giá rule-based', }) @ApiResponse({ status: 200, description: 'Kết quả đánh giá tình trạng và ước tính giá' }) @ApiResponse({ status: 429, description: 'Rate limit exceeded' }) @EndpointRateLimit({ limit: 5, windowSeconds: 60, keyStrategy: 'ip' }) @UseGuards(EndpointRateLimitGuard) @Post('estimate-from-photos') async estimateFromPhotos(@Body() dto: EstimateFromPhotosDto) { return this.commandBus.execute( new EstimateFromPhotosCommand(dto.items), ); } // ── Authenticated endpoints ─────────────────────────────────────── @ApiOperation({ summary: 'Tạo tin sang nhượng', description: 'Đăng tin sang nhượng mới' }) @ApiResponse({ status: 201, description: 'Tin sang nhượng đã tạo' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard) @Post('listings') async createListing( @CurrentUser() user: { sub: string }, @Body() dto: CreateTransferListingDto, ) { return this.commandBus.execute( new CreateTransferListingCommand( user.sub, dto.category, dto.title, dto.description ?? null, dto.address, dto.ward ?? null, dto.district, dto.city, dto.latitude, dto.longitude, BigInt(Math.round(dto.askingPriceVND)), dto.pricingSource ?? 'MANUAL', dto.isNegotiable ?? true, dto.areaM2 ?? null, dto.monthlyRentVND ? BigInt(Math.round(dto.monthlyRentVND)) : null, dto.depositMonths ?? null, dto.remainingLeaseMo ?? null, dto.businessType ?? null, dto.footTraffic ?? null, dto.contactPhone ?? null, dto.contactName ?? null, (dto.items ?? []).map((item) => ({ name: item.name, brand: item.brand, modelName: item.modelName, category: item.category, condition: item.condition, purchaseYear: item.purchaseYear, originalPriceVND: item.originalPriceVND != null ? BigInt(Math.round(item.originalPriceVND)) : undefined, askingPriceVND: BigInt(Math.round(item.askingPriceVND)), quantity: item.quantity, dimensions: item.dimensions, notes: item.notes, })), ), ); } @ApiOperation({ summary: 'Cập nhật tin sang nhượng', description: 'Cập nhật thông tin tin sang nhượng' }) @ApiResponse({ status: 200, description: 'Tin sang nhượng đã cập nhật' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard) @Patch('listings/:id') async updateListing( @Param('id') id: string, @Body() dto: UpdateTransferListingDto, ) { return this.commandBus.execute( new UpdateTransferListingCommand( id, dto.title, dto.description, dto.status, dto.askingPriceVND ? BigInt(Math.round(dto.askingPriceVND)) : undefined, dto.isNegotiable, dto.areaM2, dto.monthlyRentVND !== undefined ? BigInt(Math.round(dto.monthlyRentVND)) : undefined, dto.depositMonths, dto.remainingLeaseMo, dto.businessType, dto.footTraffic, dto.contactPhone, dto.contactName, dto.media, ), ); } }