feat(industrial): add IndustrialListing CRUD endpoints + Typesense indexing
Wire full DDD stack for IndustrialListing: domain entity, repository interface, CQRS commands/queries with handlers, Prisma repository, Typesense sync on create/update/delete, controller with 5 REST endpoints, and validated DTOs. Register all providers in IndustrialModule. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IndustrialLeaseType, IndustrialPropertyType } from '@prisma/client';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsDateString,
|
||||
Min,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateIndustrialListingDto {
|
||||
@ApiProperty({ description: 'ID khu công nghiệp' })
|
||||
@IsString()
|
||||
parkId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ID môi giới' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
agentId?: string;
|
||||
|
||||
@ApiProperty({ enum: IndustrialPropertyType, example: 'READY_BUILT_FACTORY' })
|
||||
@IsEnum(IndustrialPropertyType)
|
||||
propertyType!: IndustrialPropertyType;
|
||||
|
||||
@ApiProperty({ enum: IndustrialLeaseType, example: 'FACTORY_LEASE' })
|
||||
@IsEnum(IndustrialLeaseType)
|
||||
leaseType!: IndustrialLeaseType;
|
||||
|
||||
@ApiProperty({ example: 'Nhà xưởng 5000m² tại KCN VSIP', description: 'Tiêu đề' })
|
||||
@IsString()
|
||||
@MaxLength(300)
|
||||
title!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Mô tả chi tiết' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: 5000, description: 'Diện tích (m²)' })
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@Min(0)
|
||||
areaM2!: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 12, description: 'Chiều cao trần (m)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
ceilingHeightM?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 3, description: 'Tải trọng sàn (tấn/m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
floorLoadTonM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 12, description: 'Khoảng cách cột (m)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
columnSpacingM?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 4, description: 'Số dock' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
dockCount?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 10, description: 'Tải trọng cẩu trục (tấn)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
craneCapacityTon?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasMezzanine?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasOfficeArea?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: 200, description: 'Diện tích văn phòng (m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
officeAreaM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 5.5, description: 'Giá thuê (USD/m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
priceUsdM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'usd/m2/month', description: 'Đơn vị giá' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pricingUnit?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 27500, description: 'Tổng giá thuê' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
totalLeasePrice?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 0.6, description: 'Phí quản lý' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
managementFee?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 3, description: 'Số tháng đặt cọc' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
depositMonths?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 3, description: 'Thời hạn thuê tối thiểu (năm)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
minLeaseYears?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 50, description: 'Thời hạn thuê tối đa (năm)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
maxLeaseYears?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Ngày hết hạn thuê' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseExpiry?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Ngày có thể bắt đầu thuê' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
availableFrom?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 500, description: 'Công suất điện (KVA)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
powerCapacityKva?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 100, description: 'Cấp nước (m³/ngày)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
waterSupplyM3Day?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Hình ảnh / tài liệu' })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsObject({ each: true })
|
||||
media?: Record<string, unknown>[];
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IndustrialLeaseType, IndustrialListingStatus, IndustrialPropertyType } from '@prisma/client';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
|
||||
export class SearchIndustrialListingsDto {
|
||||
@ApiPropertyOptional({ description: 'Lọc theo KCN' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parkId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: IndustrialPropertyType })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialPropertyType)
|
||||
propertyType?: IndustrialPropertyType;
|
||||
|
||||
@ApiPropertyOptional({ enum: IndustrialLeaseType })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialLeaseType)
|
||||
leaseType?: IndustrialLeaseType;
|
||||
|
||||
@ApiPropertyOptional({ enum: IndustrialListingStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialListingStatus)
|
||||
status?: IndustrialListingStatus;
|
||||
|
||||
@ApiPropertyOptional({ example: 1000, description: 'Diện tích tối thiểu (m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@Min(0)
|
||||
minAreaM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 10000, description: 'Diện tích tối đa (m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
maxAreaM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 3, description: 'Giá tối thiểu (USD/m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
minPriceUsdM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 10, description: 'Giá tối đa (USD/m²)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
maxPriceUsdM2?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'nhà xưởng', description: 'Từ khóa tìm kiếm' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
q?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 1, default: 1 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 20, default: 20 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IndustrialLeaseType, IndustrialListingStatus, IndustrialPropertyType } from '@prisma/client';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsDateString,
|
||||
Min,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateIndustrialListingDto {
|
||||
@ApiPropertyOptional({ enum: IndustrialPropertyType })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialPropertyType)
|
||||
propertyType?: IndustrialPropertyType;
|
||||
|
||||
@ApiPropertyOptional({ enum: IndustrialLeaseType })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialLeaseType)
|
||||
leaseType?: IndustrialLeaseType;
|
||||
|
||||
@ApiPropertyOptional({ enum: IndustrialListingStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(IndustrialListingStatus)
|
||||
status?: IndustrialListingStatus;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(300)
|
||||
title?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@Min(0)
|
||||
areaM2?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
ceilingHeightM?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
floorLoadTonM2?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
columnSpacingM?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
dockCount?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
craneCapacityTon?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasMezzanine?: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasOfficeArea?: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
officeAreaM2?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
priceUsdM2?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pricingUnit?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
totalLeasePrice?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
managementFee?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
depositMonths?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
minLeaseYears?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
maxLeaseYears?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseExpiry?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
availableFrom?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
powerCapacityKva?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
waterSupplyM3Day?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsObject({ each: true })
|
||||
media?: Record<string, unknown>[];
|
||||
}
|
||||
Reference in New Issue
Block a user