import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UserRole } from '@prisma/client'; import { CurrentUser, JwtAuthGuard, Roles, RolesGuard } from '@modules/auth'; import type { JwtPayload } from '@modules/auth'; import { NotFoundException } from '@modules/shared'; import { AnalyzeIndustrialLocationQuery } from '../../application/queries/analyze-industrial-location/analyze-industrial-location.query'; import { CreateIndustrialParkCommand } from '../../application/commands/create-industrial-park/create-industrial-park.command'; import { DeleteIndustrialParkCommand } from '../../application/commands/delete-industrial-park/delete-industrial-park.command'; import { EstimateIndustrialRentQuery } from '../../application/queries/estimate-industrial-rent/estimate-industrial-rent.query'; import { UpdateIndustrialParkCommand } from '../../application/commands/update-industrial-park/update-industrial-park.command'; import { CompareIndustrialParksQuery } from '../../application/queries/compare-industrial-parks/compare-industrial-parks.query'; import { GetIndustrialParkQuery } from '../../application/queries/get-industrial-park/get-industrial-park.query'; import { IndustrialMarketQuery } from '../../application/queries/industrial-market/industrial-market.query'; import { IndustrialParkStatsQuery } from '../../application/queries/industrial-park-stats/industrial-park-stats.query'; import { ListIndustrialParksQuery } from '../../application/queries/list-industrial-parks/list-industrial-parks.query'; import { AnalyzeIndustrialLocationDto } from '../dto/analyze-industrial-location.dto'; import { CompareIndustrialParksDto } from '../dto/compare-industrial-parks.dto'; import { CreateIndustrialParkDto } from '../dto/create-industrial-park.dto'; import { EstimateIndustrialRentDto } from '../dto/estimate-industrial-rent.dto'; import { SearchIndustrialParksDto } from '../dto/search-industrial-parks.dto'; import { UpdateIndustrialParkDto } from '../dto/update-industrial-park.dto'; @ApiTags('industrial-parks') @Controller('industrial') export class IndustrialParksController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} // ── Public endpoints ────────────────────────────────────────────── @ApiOperation({ summary: 'Danh sách KCN', description: 'Tìm kiếm và lọc khu công nghiệp' }) @ApiResponse({ status: 200, description: 'Danh sách KCN phân trang' }) @Get('parks') async listParks(@Query() dto: SearchIndustrialParksDto) { return this.queryBus.execute( new ListIndustrialParksQuery( dto.q, dto.province, dto.region, dto.status, dto.minAreaHa, dto.maxRentUsdM2, dto.targetIndustry, dto.page ?? 1, dto.limit ?? 20, ), ); } // ── Park Operator endpoints ─────────────────────────────────────── @ApiOperation({ summary: 'KCN của tôi (Park Operator)', description: 'Danh sách KCN mà user hiện tại vận hành. ADMIN dùng endpoint này để xem KCN đã được gán cho mình.', }) @ApiResponse({ status: 200, description: 'Danh sách KCN đã lọc theo owner' }) @ApiResponse({ status: 401, description: 'Chưa xác thực' }) @ApiResponse({ status: 403, description: 'Không có quyền' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.PARK_OPERATOR, UserRole.ADMIN) @Get('parks/mine/list') async listMyParks( @CurrentUser() user: JwtPayload, @Query() dto: SearchIndustrialParksDto, ) { return this.queryBus.execute( new ListIndustrialParksQuery( dto.q, dto.province, dto.region, dto.status, dto.minAreaHa, dto.maxRentUsdM2, dto.targetIndustry, dto.page ?? 1, dto.limit ?? 20, user.sub, ), ); } @ApiOperation({ summary: 'Chi tiết KCN', description: 'Xem chi tiết KCN theo slug hoặc ID' }) @ApiResponse({ status: 200, description: 'Thông tin chi tiết KCN' }) @ApiResponse({ status: 404, description: 'Không tìm thấy KCN' }) @Get('parks/:slugOrId') async getPark(@Param('slugOrId') slugOrId: string) { const result = await this.queryBus.execute(new GetIndustrialParkQuery(slugOrId)); if (!result) { throw new NotFoundException('Industrial park', slugOrId); } return result; } @ApiOperation({ summary: 'So sánh KCN', description: 'So sánh 2-5 KCN' }) @ApiResponse({ status: 200, description: 'Dữ liệu so sánh KCN' }) @Post('parks/compare') async compareParks(@Body() dto: CompareIndustrialParksDto) { return this.queryBus.execute(new CompareIndustrialParksQuery(dto.ids)); } @ApiOperation({ summary: 'Thống kê KCN', description: 'Tổng quan thống kê tất cả KCN' }) @ApiResponse({ status: 200, description: 'Dữ liệu thống kê' }) @Get('parks/stats') async getStats() { return this.queryBus.execute(new IndustrialParkStatsQuery()); } @ApiOperation({ summary: 'Thị trường KCN', description: 'Dữ liệu thị trường BĐS công nghiệp' }) @ApiResponse({ status: 200, description: 'Dữ liệu thị trường' }) @Get('market') async getMarket() { return this.queryBus.execute(new IndustrialMarketQuery()); } @ApiOperation({ summary: 'Phân tích vị trí KCN', description: 'Đánh giá vị trí dựa trên hạ tầng, kết nối, lao động' }) @ApiResponse({ status: 200, description: 'Kết quả phân tích vị trí' }) @Post('analyze-location') async analyzeLocation(@Body() dto: AnalyzeIndustrialLocationDto) { return this.queryBus.execute( new AnalyzeIndustrialLocationQuery( dto.latitude, dto.longitude, dto.park_name ?? null, dto.target_industry ?? null, ), ); } @ApiOperation({ summary: 'Ước tính giá thuê KCN', description: 'Tính giá thuê BĐS công nghiệp theo tỉnh, loại, diện tích' }) @ApiResponse({ status: 200, description: 'Kết quả ước tính giá thuê' }) @Post('estimate-rent') async estimateRent(@Body() dto: EstimateIndustrialRentDto) { return this.queryBus.execute( new EstimateIndustrialRentQuery( dto.province, dto.property_type, dto.area_m2, dto.lease_duration_years, dto.park_name ?? null, dto.requires_crane ?? false, dto.required_power_kva ?? null, dto.requires_wastewater ?? false, ), ); } // ── Admin endpoints ─────────────────────────────────────────────── @ApiOperation({ summary: 'Tạo KCN (admin)', description: 'Tạo mới khu công nghiệp' }) @ApiResponse({ status: 201, description: 'KCN đã tạo' }) @ApiResponse({ status: 400, description: 'Validation error' }) @ApiResponse({ status: 401, description: 'Chưa xác thực' }) @ApiResponse({ status: 403, description: 'Không có quyền' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN, UserRole.PARK_OPERATOR) @Post('parks') async createPark( @CurrentUser() user: JwtPayload, @Body() dto: CreateIndustrialParkDto, ) { // PARK_OPERATOR callers own what they create; admin may leave unassigned. const ownerId = user.role === UserRole.PARK_OPERATOR ? user.sub : null; return this.commandBus.execute( new CreateIndustrialParkCommand( dto.name, dto.nameEn ?? null, dto.slug, dto.developer, dto.operator ?? null, dto.status, dto.latitude, dto.longitude, dto.address, dto.district, dto.province, dto.region, dto.totalAreaHa, dto.leasableAreaHa, dto.occupancyRate, dto.remainingAreaHa, dto.tenantCount ?? 0, dto.establishedYear ?? null, dto.landRentUsdM2Year ?? null, dto.rbfRentUsdM2Month ?? null, dto.rbwRentUsdM2Month ?? null, dto.managementFeeUsd ?? null, dto.infrastructure ?? null, dto.connectivity ?? null, dto.incentives ?? null, dto.targetIndustries, dto.description ?? null, dto.descriptionEn ?? null, ownerId, ), ); } @ApiOperation({ summary: 'Cập nhật KCN (admin)', description: 'Cập nhật thông tin KCN' }) @ApiResponse({ status: 200, description: 'KCN đã cập nhật' }) @ApiResponse({ status: 401, description: 'Chưa xác thực' }) @ApiResponse({ status: 403, description: 'Không có quyền' }) @ApiResponse({ status: 404, description: 'Không tìm thấy tài nguyên' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN, UserRole.PARK_OPERATOR) @Patch('parks/:id') async updatePark( @CurrentUser() user: JwtPayload, @Param('id') id: string, @Body() dto: UpdateIndustrialParkDto, ) { return this.commandBus.execute( new UpdateIndustrialParkCommand( id, user.sub, user.role as UserRole, dto.name, dto.nameEn, dto.developer, dto.operator, dto.status, dto.occupancyRate, dto.remainingAreaHa, dto.tenantCount, dto.landRentUsdM2Year, dto.rbfRentUsdM2Month, dto.rbwRentUsdM2Month, dto.managementFeeUsd, dto.infrastructure, dto.connectivity, dto.incentives, dto.targetIndustries, dto.description, dto.descriptionEn, dto.isVerified, dto.ownerId, ), ); } @ApiOperation({ summary: 'Xóa KCN (admin)', description: 'Xóa vĩnh viễn khu công nghiệp' }) @ApiResponse({ status: 200, description: 'KCN đã xóa' }) @ApiResponse({ status: 401, description: 'Chưa xác thực' }) @ApiResponse({ status: 403, description: 'Không có quyền' }) @ApiResponse({ status: 404, description: 'Không tìm thấy tài nguyên' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN, UserRole.PARK_OPERATOR) @Delete('parks/:id') async deletePark( @CurrentUser() user: JwtPayload, @Param('id') id: string, ): Promise<{ success: true }> { await this.commandBus.execute( new DeleteIndustrialParkCommand(id, user.sub, user.role as UserRole), ); return { success: true }; } }