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:
Ho Ngoc Hai
2026-04-16 09:11:16 +07:00
parent 7ce651fce5
commit deb04989de
123 changed files with 8260 additions and 12 deletions

View File

@@ -0,0 +1,172 @@
import { type IndustrialParkStatus, type VietnamRegion } from '@prisma/client';
import { AggregateRoot } from '@modules/shared';
export interface IndustrialParkProps {
name: string;
nameEn: string | null;
slug: string;
developer: string;
operator: string | null;
status: IndustrialParkStatus;
latitude: number;
longitude: number;
address: string;
district: string;
province: string;
region: VietnamRegion;
totalAreaHa: number;
leasableAreaHa: number;
occupancyRate: number;
remainingAreaHa: number;
tenantCount: number;
establishedYear: number | null;
landRentUsdM2Year: number | null;
rbfRentUsdM2Month: number | null;
rbwRentUsdM2Month: number | null;
managementFeeUsd: number | null;
infrastructure: Record<string, unknown> | null;
connectivity: Record<string, unknown> | null;
incentives: Record<string, unknown> | null;
targetIndustries: string[];
existingTenants: Record<string, unknown>[] | null;
certifications: string[] | null;
media: Record<string, unknown>[] | null;
documents: Record<string, unknown>[] | null;
description: string | null;
descriptionEn: string | null;
isVerified: boolean;
}
export class IndustrialParkEntity extends AggregateRoot<string> {
private _name: string;
private _nameEn: string | null;
private _slug: string;
private _developer: string;
private _operator: string | null;
private _status: IndustrialParkStatus;
private _latitude: number;
private _longitude: number;
private _address: string;
private _district: string;
private _province: string;
private _region: VietnamRegion;
private _totalAreaHa: number;
private _leasableAreaHa: number;
private _occupancyRate: number;
private _remainingAreaHa: number;
private _tenantCount: number;
private _establishedYear: number | null;
private _landRentUsdM2Year: number | null;
private _rbfRentUsdM2Month: number | null;
private _rbwRentUsdM2Month: number | null;
private _managementFeeUsd: number | null;
private _infrastructure: Record<string, unknown> | null;
private _connectivity: Record<string, unknown> | null;
private _incentives: Record<string, unknown> | null;
private _targetIndustries: string[];
private _existingTenants: Record<string, unknown>[] | null;
private _certifications: string[] | null;
private _media: Record<string, unknown>[] | null;
private _documents: Record<string, unknown>[] | null;
private _description: string | null;
private _descriptionEn: string | null;
private _isVerified: boolean;
constructor(id: string, props: IndustrialParkProps, createdAt: Date, updatedAt: Date) {
super(id, createdAt, updatedAt);
this._name = props.name;
this._nameEn = props.nameEn;
this._slug = props.slug;
this._developer = props.developer;
this._operator = props.operator;
this._status = props.status;
this._latitude = props.latitude;
this._longitude = props.longitude;
this._address = props.address;
this._district = props.district;
this._province = props.province;
this._region = props.region;
this._totalAreaHa = props.totalAreaHa;
this._leasableAreaHa = props.leasableAreaHa;
this._occupancyRate = props.occupancyRate;
this._remainingAreaHa = props.remainingAreaHa;
this._tenantCount = props.tenantCount;
this._establishedYear = props.establishedYear;
this._landRentUsdM2Year = props.landRentUsdM2Year;
this._rbfRentUsdM2Month = props.rbfRentUsdM2Month;
this._rbwRentUsdM2Month = props.rbwRentUsdM2Month;
this._managementFeeUsd = props.managementFeeUsd;
this._infrastructure = props.infrastructure;
this._connectivity = props.connectivity;
this._incentives = props.incentives;
this._targetIndustries = props.targetIndustries;
this._existingTenants = props.existingTenants;
this._certifications = props.certifications;
this._media = props.media;
this._documents = props.documents;
this._description = props.description;
this._descriptionEn = props.descriptionEn;
this._isVerified = props.isVerified;
}
get name() { return this._name; }
get nameEn() { return this._nameEn; }
get slug() { return this._slug; }
get developer() { return this._developer; }
get operator() { return this._operator; }
get status() { return this._status; }
get latitude() { return this._latitude; }
get longitude() { return this._longitude; }
get address() { return this._address; }
get district() { return this._district; }
get province() { return this._province; }
get region() { return this._region; }
get totalAreaHa() { return this._totalAreaHa; }
get leasableAreaHa() { return this._leasableAreaHa; }
get occupancyRate() { return this._occupancyRate; }
get remainingAreaHa() { return this._remainingAreaHa; }
get tenantCount() { return this._tenantCount; }
get establishedYear() { return this._establishedYear; }
get landRentUsdM2Year() { return this._landRentUsdM2Year; }
get rbfRentUsdM2Month() { return this._rbfRentUsdM2Month; }
get rbwRentUsdM2Month() { return this._rbwRentUsdM2Month; }
get managementFeeUsd() { return this._managementFeeUsd; }
get infrastructure() { return this._infrastructure; }
get connectivity() { return this._connectivity; }
get incentives() { return this._incentives; }
get targetIndustries() { return this._targetIndustries; }
get existingTenants() { return this._existingTenants; }
get certifications() { return this._certifications; }
get media() { return this._media; }
get documents() { return this._documents; }
get description() { return this._description; }
get descriptionEn() { return this._descriptionEn; }
get isVerified() { return this._isVerified; }
updateDetails(props: Partial<IndustrialParkProps>): void {
if (props.name !== undefined) this._name = props.name;
if (props.nameEn !== undefined) this._nameEn = props.nameEn;
if (props.developer !== undefined) this._developer = props.developer;
if (props.operator !== undefined) this._operator = props.operator;
if (props.status !== undefined) this._status = props.status;
if (props.occupancyRate !== undefined) this._occupancyRate = props.occupancyRate;
if (props.remainingAreaHa !== undefined) this._remainingAreaHa = props.remainingAreaHa;
if (props.tenantCount !== undefined) this._tenantCount = props.tenantCount;
if (props.landRentUsdM2Year !== undefined) this._landRentUsdM2Year = props.landRentUsdM2Year;
if (props.rbfRentUsdM2Month !== undefined) this._rbfRentUsdM2Month = props.rbfRentUsdM2Month;
if (props.rbwRentUsdM2Month !== undefined) this._rbwRentUsdM2Month = props.rbwRentUsdM2Month;
if (props.managementFeeUsd !== undefined) this._managementFeeUsd = props.managementFeeUsd;
if (props.infrastructure !== undefined) this._infrastructure = props.infrastructure;
if (props.connectivity !== undefined) this._connectivity = props.connectivity;
if (props.incentives !== undefined) this._incentives = props.incentives;
if (props.targetIndustries !== undefined) this._targetIndustries = props.targetIndustries;
if (props.existingTenants !== undefined) this._existingTenants = props.existingTenants;
if (props.certifications !== undefined) this._certifications = props.certifications;
if (props.media !== undefined) this._media = props.media;
if (props.documents !== undefined) this._documents = props.documents;
if (props.description !== undefined) this._description = props.description;
if (props.descriptionEn !== undefined) this._descriptionEn = props.descriptionEn;
if (props.isVerified !== undefined) this._isVerified = props.isVerified;
this.updatedAt = new Date();
}
}

View File

@@ -0,0 +1,117 @@
import type { IndustrialParkStatus, VietnamRegion } from '@prisma/client';
import type { IndustrialParkEntity } from '../entities/industrial-park.entity';
export const INDUSTRIAL_PARK_REPOSITORY = Symbol('INDUSTRIAL_PARK_REPOSITORY');
export interface IndustrialParkSearchParams {
query?: string;
province?: string;
region?: VietnamRegion;
status?: IndustrialParkStatus;
minAreaHa?: number;
maxRentUsdM2?: number;
targetIndustry?: string;
page?: number;
limit?: number;
}
export interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface IndustrialParkListItem {
id: string;
name: string;
nameEn: string | null;
slug: string;
developer: string;
status: IndustrialParkStatus;
province: string;
region: VietnamRegion;
totalAreaHa: number;
occupancyRate: number;
remainingAreaHa: number;
tenantCount: number;
landRentUsdM2Year: number | null;
rbfRentUsdM2Month: number | null;
rbwRentUsdM2Month: number | null;
targetIndustries: string[];
latitude: number;
longitude: number;
}
export interface IndustrialParkDetailData {
id: string;
name: string;
nameEn: string | null;
slug: string;
developer: string;
operator: string | null;
status: IndustrialParkStatus;
latitude: number;
longitude: number;
address: string;
district: string;
province: string;
region: VietnamRegion;
totalAreaHa: number;
leasableAreaHa: number;
occupancyRate: number;
remainingAreaHa: number;
tenantCount: number;
establishedYear: number | null;
landRentUsdM2Year: number | null;
rbfRentUsdM2Month: number | null;
rbwRentUsdM2Month: number | null;
managementFeeUsd: number | null;
infrastructure: Record<string, unknown> | null;
connectivity: Record<string, unknown> | null;
incentives: Record<string, unknown> | null;
targetIndustries: string[];
existingTenants: Record<string, unknown>[] | null;
certifications: string[] | null;
media: Record<string, unknown>[] | null;
documents: Record<string, unknown>[] | null;
description: string | null;
descriptionEn: string | null;
isVerified: boolean;
listingCount: number;
createdAt: Date;
updatedAt: Date;
}
export interface IndustrialParkStatsData {
totalParks: number;
totalAreaHa: number;
avgOccupancyRate: number;
totalTenants: number;
byRegion: { region: string; count: number; avgOccupancy: number }[];
byStatus: { status: string; count: number }[];
topProvinces: { province: string; count: number; avgRent: number | null }[];
}
export interface IndustrialMarketData {
totalParks: number;
avgOccupancyRate: number;
avgLandRentUsdM2: number | null;
avgRbfRentUsdM2: number | null;
rentByRegion: { region: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[];
rentByProvince: { province: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[];
}
export interface IIndustrialParkRepository {
findById(id: string): Promise<IndustrialParkEntity | null>;
findBySlug(slug: string): Promise<IndustrialParkEntity | null>;
findDetailBySlug(slug: string): Promise<IndustrialParkDetailData | null>;
findDetailById(id: string): Promise<IndustrialParkDetailData | null>;
save(entity: IndustrialParkEntity): Promise<void>;
update(entity: IndustrialParkEntity): Promise<void>;
search(params: IndustrialParkSearchParams): Promise<PaginatedResult<IndustrialParkListItem>>;
compareParks(ids: string[]): Promise<IndustrialParkDetailData[]>;
getStats(): Promise<IndustrialParkStatsData>;
getMarketData(): Promise<IndustrialMarketData>;
}