feat(api): add industrial, transfer, and reports backend modules
Add three new NestJS modules following DDD/CQRS architecture: - Industrial: KCN (industrial park) management with PostGIS geo queries, Typesense search, and market statistics - Transfer: Furniture/premises transfer listings with AI-powered price estimation and depreciation modeling - Reports: Async AI report generation via BullMQ with Claude narrative service, PDF generation, and macro data integration Includes Prisma schema models, migrations, seed scripts, and app.module wiring with BullMQ Redis config. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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 { UserRole } from '@prisma/client';
|
||||
import { JwtAuthGuard, Roles, RolesGuard } from '@modules/auth';
|
||||
import { NotFoundException } from '@modules/shared';
|
||||
import { CreateIndustrialParkCommand } from '../../application/commands/create-industrial-park/create-industrial-park.command';
|
||||
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 { type CompareIndustrialParksDto } from '../dto/compare-industrial-parks.dto';
|
||||
import { type CreateIndustrialParkDto } from '../dto/create-industrial-park.dto';
|
||||
import { type SearchIndustrialParksDto } from '../dto/search-industrial-parks.dto';
|
||||
import { type 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
// ── 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' })
|
||||
@ApiBearerAuth('JWT')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@Post('parks')
|
||||
async createPark(@Body() dto: CreateIndustrialParkDto) {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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' })
|
||||
@ApiBearerAuth('JWT')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@Patch('parks/:id')
|
||||
async updatePark(@Param('id') id: string, @Body() dto: UpdateIndustrialParkDto) {
|
||||
return this.commandBus.execute(
|
||||
new UpdateIndustrialParkCommand(
|
||||
id,
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user