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:
@@ -68,6 +68,8 @@ model User {
|
||||
buyerOrders Order[] @relation("BuyerOrders")
|
||||
sellerOrders Order[] @relation("SellerOrders")
|
||||
mfaChallenges MfaChallenge[]
|
||||
transferListings TransferListing[]
|
||||
reports Report[]
|
||||
|
||||
@@index([role])
|
||||
@@index([kycStatus])
|
||||
@@ -623,6 +625,7 @@ model Plan {
|
||||
maxListings Int?
|
||||
maxSavedSearches Int?
|
||||
maxAnalyticsQueries Int?
|
||||
maxReports Int?
|
||||
maxMediaUploads Int?
|
||||
features Json
|
||||
isActive Boolean @default(true)
|
||||
@@ -1089,3 +1092,203 @@ model Message {
|
||||
@@index([conversationId, createdAt])
|
||||
@@index([senderId])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRANSFER (Furniture + Premises Handover)
|
||||
// =============================================================================
|
||||
|
||||
enum TransferCategory {
|
||||
FURNITURE // Nội thất (sofa, bàn, tủ, giường)
|
||||
APPLIANCE // Thiết bị gia dụng (máy lạnh, tủ lạnh, máy giặt)
|
||||
OFFICE_EQUIPMENT // Thiết bị văn phòng (bàn làm việc, ghế, máy in)
|
||||
KITCHEN // Bếp + thiết bị bếp
|
||||
PREMISES // Mặt bằng kinh doanh
|
||||
FULL_UNIT // Chuyển nhượng trọn bộ (nội thất + mặt bằng)
|
||||
}
|
||||
|
||||
enum TransferCondition {
|
||||
NEW // Mới (< 6 tháng)
|
||||
LIKE_NEW // Như mới (6-12 tháng)
|
||||
GOOD // Tốt (1-3 năm)
|
||||
FAIR // Khá (3-5 năm)
|
||||
WORN // Cũ (> 5 năm)
|
||||
}
|
||||
|
||||
enum TransferListingStatus {
|
||||
DRAFT
|
||||
PENDING_REVIEW
|
||||
ACTIVE
|
||||
RESERVED
|
||||
SOLD
|
||||
EXPIRED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum TransferPricingSource {
|
||||
MANUAL // Người bán tự định giá
|
||||
AI_ESTIMATED // AI ước tính dựa trên khấu hao + thương hiệu
|
||||
NEGOTIABLE // Giá thương lượng
|
||||
}
|
||||
|
||||
model TransferListing {
|
||||
id String @id @default(cuid())
|
||||
sellerId String
|
||||
seller User @relation(fields: [sellerId], references: [id], onDelete: Restrict)
|
||||
category TransferCategory
|
||||
status TransferListingStatus @default(DRAFT)
|
||||
title String
|
||||
description String? @db.Text
|
||||
// Location
|
||||
address String
|
||||
ward String?
|
||||
district String
|
||||
city String
|
||||
location Unsupported("geometry(Point, 4326)")
|
||||
// Pricing
|
||||
askingPriceVND BigInt
|
||||
aiEstimatePriceVND BigInt?
|
||||
aiConfidence Float?
|
||||
pricingSource TransferPricingSource @default(MANUAL)
|
||||
isNegotiable Boolean @default(true)
|
||||
// Premises-specific fields (for PREMISES / FULL_UNIT)
|
||||
areaM2 Float?
|
||||
monthlyRentVND BigInt?
|
||||
depositMonths Int?
|
||||
remainingLeaseMo Int?
|
||||
businessType String? // Loại hình kinh doanh hiện tại
|
||||
footTraffic String? // Mô tả lưu lượng khách
|
||||
// Metadata
|
||||
media Json? // [{ url, type, order, caption }]
|
||||
moderationScore Float?
|
||||
moderationNotes String?
|
||||
viewCount Int @default(0)
|
||||
saveCount Int @default(0)
|
||||
inquiryCount Int @default(0)
|
||||
contactPhone String?
|
||||
contactName String?
|
||||
featuredUntil DateTime?
|
||||
expiresAt DateTime?
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
items TransferItem[]
|
||||
|
||||
@@index([sellerId])
|
||||
@@index([category])
|
||||
@@index([status])
|
||||
@@index([district, city])
|
||||
@@index([askingPriceVND])
|
||||
@@index([location], type: Gist)
|
||||
@@index([publishedAt])
|
||||
@@index([createdAt])
|
||||
@@index([featuredUntil])
|
||||
@@index([expiresAt])
|
||||
@@index([category, status, publishedAt(sort: Desc)])
|
||||
@@index([district, city, category, status])
|
||||
@@index([status, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
model TransferItem {
|
||||
id String @id @default(cuid())
|
||||
transferListingId String
|
||||
transferListing TransferListing @relation(fields: [transferListingId], references: [id], onDelete: Cascade)
|
||||
name String // Tên sản phẩm (e.g. "Sofa góc L 3m")
|
||||
brand String? // Thương hiệu
|
||||
modelName String? // Model / SKU
|
||||
category TransferCategory
|
||||
condition TransferCondition
|
||||
purchaseYear Int? // Năm mua
|
||||
originalPriceVND BigInt? // Giá mua ban đầu
|
||||
askingPriceVND BigInt // Giá bán mong muốn
|
||||
aiEstimatePriceVND BigInt? // AI ước tính
|
||||
aiConfidence Float?
|
||||
quantity Int @default(1)
|
||||
dimensions Json? // { widthCm, heightCm, depthCm, weightKg }
|
||||
media Json? // [{ url, type, order }]
|
||||
notes String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([transferListingId])
|
||||
@@index([category])
|
||||
@@index([condition])
|
||||
@@index([brand])
|
||||
@@index([askingPriceVND])
|
||||
@@index([transferListingId, category])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AI REPORTS
|
||||
// =============================================================================
|
||||
|
||||
enum ReportType {
|
||||
RESIDENTIAL_MARKET
|
||||
INDUSTRIAL_MARKET
|
||||
DISTRICT_ANALYSIS
|
||||
INVESTMENT_FEASIBILITY
|
||||
INDUSTRIAL_LOCATION
|
||||
PROPERTY_VALUATION
|
||||
PORTFOLIO
|
||||
}
|
||||
|
||||
enum ReportStatus {
|
||||
GENERATING
|
||||
READY
|
||||
FAILED
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
type ReportType
|
||||
title String
|
||||
params Json // Input parameters (city, province, period, etc.)
|
||||
content Json? // Structured report content (sections, charts data)
|
||||
pdfUrl String? // MinIO URL to generated PDF
|
||||
status ReportStatus @default(GENERATING)
|
||||
errorMsg String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId, createdAt(sort: Desc)])
|
||||
@@index([userId, type])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model MacroeconomicData {
|
||||
id String @id @default(cuid())
|
||||
province String
|
||||
indicator String // gdp, fdi, population, urbanization, labor_force, avg_wage, industrial_output, cpi, mortgage_rate
|
||||
value Float
|
||||
unit String // USD, VND, %, persons, etc.
|
||||
period String // e.g. "2025", "2025-Q4"
|
||||
source String // GSO, World Bank, SBV
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([province, indicator, period])
|
||||
@@index([province])
|
||||
@@index([indicator, period])
|
||||
}
|
||||
|
||||
model InfrastructureProject {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
province String
|
||||
category String // metro, highway, airport, port, bridge, industrial_zone
|
||||
status String // planning, under_construction, completed
|
||||
investmentVND BigInt?
|
||||
startDate DateTime?
|
||||
completionDate DateTime?
|
||||
description String? @db.Text
|
||||
impactRadius Float? // km
|
||||
location Unsupported("geometry(Point, 4326)")?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([province])
|
||||
@@index([category])
|
||||
@@index([status])
|
||||
@@index([province, category])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user