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,168 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IndustrialParkStatus" AS ENUM ('PLANNING', 'UNDER_CONSTRUCTION', 'OPERATIONAL', 'FULL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IndustrialPropertyType" AS ENUM ('INDUSTRIAL_LAND', 'READY_BUILT_FACTORY', 'READY_BUILT_WAREHOUSE', 'LOGISTICS_CENTER', 'OFFICE_IN_PARK', 'DATA_CENTER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IndustrialLeaseType" AS ENUM ('LAND_LEASE', 'FACTORY_LEASE', 'WAREHOUSE_LEASE', 'SUBLEASE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IndustrialListingStatus" AS ENUM ('DRAFT', 'ACTIVE', 'RESERVED', 'LEASED', 'EXPIRED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VietnamRegion" AS ENUM ('NORTH', 'CENTRAL', 'SOUTH');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IndustrialPark" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEn" TEXT,
|
||||
"slug" TEXT NOT NULL,
|
||||
"developer" TEXT NOT NULL,
|
||||
"operator" TEXT,
|
||||
"status" "IndustrialParkStatus" NOT NULL DEFAULT 'PLANNING',
|
||||
"location" geometry(Point, 4326) NOT NULL,
|
||||
"address" TEXT NOT NULL,
|
||||
"district" TEXT NOT NULL,
|
||||
"province" TEXT NOT NULL,
|
||||
"region" "VietnamRegion" NOT NULL,
|
||||
"totalAreaHa" DOUBLE PRECISION NOT NULL,
|
||||
"leasableAreaHa" DOUBLE PRECISION NOT NULL,
|
||||
"occupancyRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"remainingAreaHa" DOUBLE PRECISION NOT NULL,
|
||||
"tenantCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"establishedYear" INTEGER,
|
||||
"landRentUsdM2Year" DOUBLE PRECISION,
|
||||
"rbfRentUsdM2Month" DOUBLE PRECISION,
|
||||
"rbwRentUsdM2Month" DOUBLE PRECISION,
|
||||
"managementFeeUsd" DOUBLE PRECISION,
|
||||
"infrastructure" JSONB,
|
||||
"connectivity" JSONB,
|
||||
"incentives" JSONB,
|
||||
"targetIndustries" TEXT[],
|
||||
"existingTenants" JSONB,
|
||||
"certifications" JSONB,
|
||||
"media" JSONB,
|
||||
"documents" JSONB,
|
||||
"description" TEXT,
|
||||
"descriptionEn" TEXT,
|
||||
"isVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "IndustrialPark_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IndustrialListing" (
|
||||
"id" TEXT NOT NULL,
|
||||
"parkId" TEXT NOT NULL,
|
||||
"agentId" TEXT,
|
||||
"sellerId" TEXT NOT NULL,
|
||||
"propertyType" "IndustrialPropertyType" NOT NULL,
|
||||
"leaseType" "IndustrialLeaseType" NOT NULL,
|
||||
"status" "IndustrialListingStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"areaM2" DOUBLE PRECISION NOT NULL,
|
||||
"ceilingHeightM" DOUBLE PRECISION,
|
||||
"floorLoadTonM2" DOUBLE PRECISION,
|
||||
"columnSpacingM" DOUBLE PRECISION,
|
||||
"dockCount" INTEGER,
|
||||
"craneCapacityTon" DOUBLE PRECISION,
|
||||
"hasMezzanine" BOOLEAN NOT NULL DEFAULT false,
|
||||
"hasOfficeArea" BOOLEAN NOT NULL DEFAULT false,
|
||||
"officeAreaM2" DOUBLE PRECISION,
|
||||
"priceUsdM2" DOUBLE PRECISION,
|
||||
"pricingUnit" TEXT,
|
||||
"totalLeasePrice" DOUBLE PRECISION,
|
||||
"managementFee" DOUBLE PRECISION,
|
||||
"depositMonths" INTEGER,
|
||||
"minLeaseYears" INTEGER,
|
||||
"maxLeaseYears" INTEGER,
|
||||
"leaseExpiry" TIMESTAMP(3),
|
||||
"availableFrom" TIMESTAMP(3),
|
||||
"powerCapacityKva" DOUBLE PRECISION,
|
||||
"waterSupplyM3Day" DOUBLE PRECISION,
|
||||
"media" JSONB,
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"inquiryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "IndustrialListing_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IndustrialPark_slug_key" ON "IndustrialPark"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_status_idx" ON "IndustrialPark"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_province_idx" ON "IndustrialPark"("province");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_region_idx" ON "IndustrialPark"("region");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_developer_idx" ON "IndustrialPark"("developer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_location_idx" ON "IndustrialPark" USING GIST ("location");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_isVerified_idx" ON "IndustrialPark"("isVerified");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_occupancyRate_idx" ON "IndustrialPark"("occupancyRate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_landRentUsdM2Year_idx" ON "IndustrialPark"("landRentUsdM2Year");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_region_province_status_idx" ON "IndustrialPark"("region", "province", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialPark_createdAt_idx" ON "IndustrialPark"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_parkId_idx" ON "IndustrialListing"("parkId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_propertyType_idx" ON "IndustrialListing"("propertyType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_leaseType_idx" ON "IndustrialListing"("leaseType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_status_idx" ON "IndustrialListing"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_areaM2_idx" ON "IndustrialListing"("areaM2");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_priceUsdM2_idx" ON "IndustrialListing"("priceUsdM2");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_sellerId_idx" ON "IndustrialListing"("sellerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_agentId_idx" ON "IndustrialListing"("agentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_publishedAt_idx" ON "IndustrialListing"("publishedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_parkId_status_idx" ON "IndustrialListing"("parkId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_propertyType_leaseType_status_idx" ON "IndustrialListing"("propertyType", "leaseType", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IndustrialListing_status_publishedAt_idx" ON "IndustrialListing"("status", "publishedAt" DESC);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IndustrialListing" ADD CONSTRAINT "IndustrialListing_parkId_fkey" FOREIGN KEY ("parkId") REFERENCES "IndustrialPark"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,105 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransferCategory" AS ENUM ('FURNITURE', 'APPLIANCE', 'OFFICE_EQUIPMENT', 'KITCHEN', 'PREMISES', 'FULL_UNIT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransferCondition" AS ENUM ('NEW', 'LIKE_NEW', 'GOOD', 'FAIR', 'WORN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransferListingStatus" AS ENUM ('DRAFT', 'PENDING_REVIEW', 'ACTIVE', 'RESERVED', 'SOLD', 'EXPIRED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransferPricingSource" AS ENUM ('MANUAL', 'AI_ESTIMATED', 'NEGOTIABLE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TransferListing" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sellerId" TEXT NOT NULL,
|
||||
"category" "TransferCategory" NOT NULL,
|
||||
"status" "TransferListingStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"address" TEXT NOT NULL,
|
||||
"ward" TEXT,
|
||||
"district" TEXT NOT NULL,
|
||||
"city" TEXT NOT NULL,
|
||||
"location" geometry(Point, 4326) NOT NULL,
|
||||
"askingPriceVND" BIGINT NOT NULL,
|
||||
"aiEstimatePriceVND" BIGINT,
|
||||
"aiConfidence" DOUBLE PRECISION,
|
||||
"pricingSource" "TransferPricingSource" NOT NULL DEFAULT 'MANUAL',
|
||||
"isNegotiable" BOOLEAN NOT NULL DEFAULT true,
|
||||
"areaM2" DOUBLE PRECISION,
|
||||
"monthlyRentVND" BIGINT,
|
||||
"depositMonths" INTEGER,
|
||||
"remainingLeaseMo" INTEGER,
|
||||
"businessType" TEXT,
|
||||
"footTraffic" TEXT,
|
||||
"media" JSONB,
|
||||
"moderationScore" DOUBLE PRECISION,
|
||||
"moderationNotes" TEXT,
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"saveCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"inquiryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"contactPhone" TEXT,
|
||||
"contactName" TEXT,
|
||||
"featuredUntil" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TransferListing_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TransferItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"transferListingId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"brand" TEXT,
|
||||
"modelName" TEXT,
|
||||
"category" "TransferCategory" NOT NULL,
|
||||
"condition" "TransferCondition" NOT NULL,
|
||||
"purchaseYear" INTEGER,
|
||||
"originalPriceVND" BIGINT,
|
||||
"askingPriceVND" BIGINT NOT NULL,
|
||||
"aiEstimatePriceVND" BIGINT,
|
||||
"aiConfidence" DOUBLE PRECISION,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
"dimensions" JSONB,
|
||||
"media" JSONB,
|
||||
"notes" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TransferItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: TransferListing
|
||||
CREATE INDEX "TransferListing_sellerId_idx" ON "TransferListing"("sellerId");
|
||||
CREATE INDEX "TransferListing_category_idx" ON "TransferListing"("category");
|
||||
CREATE INDEX "TransferListing_status_idx" ON "TransferListing"("status");
|
||||
CREATE INDEX "TransferListing_district_city_idx" ON "TransferListing"("district", "city");
|
||||
CREATE INDEX "TransferListing_askingPriceVND_idx" ON "TransferListing"("askingPriceVND");
|
||||
CREATE INDEX "TransferListing_location_idx" ON "TransferListing" USING GIST ("location");
|
||||
CREATE INDEX "TransferListing_publishedAt_idx" ON "TransferListing"("publishedAt");
|
||||
CREATE INDEX "TransferListing_createdAt_idx" ON "TransferListing"("createdAt");
|
||||
CREATE INDEX "TransferListing_featuredUntil_idx" ON "TransferListing"("featuredUntil");
|
||||
CREATE INDEX "TransferListing_expiresAt_idx" ON "TransferListing"("expiresAt");
|
||||
CREATE INDEX "TransferListing_category_status_publishedAt_idx" ON "TransferListing"("category", "status", "publishedAt" DESC);
|
||||
CREATE INDEX "TransferListing_district_city_category_status_idx" ON "TransferListing"("district", "city", "category", "status");
|
||||
CREATE INDEX "TransferListing_status_createdAt_idx" ON "TransferListing"("status", "createdAt" DESC);
|
||||
|
||||
-- CreateIndex: TransferItem
|
||||
CREATE INDEX "TransferItem_transferListingId_idx" ON "TransferItem"("transferListingId");
|
||||
CREATE INDEX "TransferItem_category_idx" ON "TransferItem"("category");
|
||||
CREATE INDEX "TransferItem_condition_idx" ON "TransferItem"("condition");
|
||||
CREATE INDEX "TransferItem_brand_idx" ON "TransferItem"("brand");
|
||||
CREATE INDEX "TransferItem_askingPriceVND_idx" ON "TransferItem"("askingPriceVND");
|
||||
CREATE INDEX "TransferItem_transferListingId_category_idx" ON "TransferItem"("transferListingId", "category");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TransferListing" ADD CONSTRAINT "TransferListing_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TransferItem" ADD CONSTRAINT "TransferItem_transferListingId_fkey" FOREIGN KEY ("transferListingId") REFERENCES "TransferListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,77 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReportType" AS ENUM ('RESIDENTIAL_MARKET', 'INDUSTRIAL_MARKET', 'DISTRICT_ANALYSIS', 'INVESTMENT_FEASIBILITY', 'INDUSTRIAL_LOCATION', 'PROPERTY_VALUATION', 'PORTFOLIO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReportStatus" AS ENUM ('GENERATING', 'READY', 'FAILED');
|
||||
|
||||
-- AlterTable: Add maxReports to Plan
|
||||
ALTER TABLE "Plan" ADD COLUMN "maxReports" INTEGER;
|
||||
|
||||
-- CreateTable: Report
|
||||
CREATE TABLE "Report" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "ReportType" NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"params" JSONB NOT NULL,
|
||||
"content" JSONB,
|
||||
"pdfUrl" TEXT,
|
||||
"status" "ReportStatus" NOT NULL DEFAULT 'GENERATING',
|
||||
"errorMsg" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable: MacroeconomicData
|
||||
CREATE TABLE "MacroeconomicData" (
|
||||
"id" TEXT NOT NULL,
|
||||
"province" TEXT NOT NULL,
|
||||
"indicator" TEXT NOT NULL,
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"unit" TEXT NOT NULL,
|
||||
"period" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MacroeconomicData_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable: InfrastructureProject
|
||||
CREATE TABLE "InfrastructureProject" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"province" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"investmentVND" BIGINT,
|
||||
"startDate" TIMESTAMP(3),
|
||||
"completionDate" TIMESTAMP(3),
|
||||
"description" TEXT,
|
||||
"impactRadius" DOUBLE PRECISION,
|
||||
"location" geometry(Point, 4326),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "InfrastructureProject_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Report_userId_createdAt_idx" ON "Report"("userId", "createdAt" DESC);
|
||||
CREATE INDEX "Report_userId_type_idx" ON "Report"("userId", "type");
|
||||
CREATE INDEX "Report_status_idx" ON "Report"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MacroeconomicData_province_indicator_period_key" ON "MacroeconomicData"("province", "indicator", "period");
|
||||
CREATE INDEX "MacroeconomicData_province_idx" ON "MacroeconomicData"("province");
|
||||
CREATE INDEX "MacroeconomicData_indicator_period_idx" ON "MacroeconomicData"("indicator", "period");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "InfrastructureProject_province_idx" ON "InfrastructureProject"("province");
|
||||
CREATE INDEX "InfrastructureProject_category_idx" ON "InfrastructureProject"("category");
|
||||
CREATE INDEX "InfrastructureProject_status_idx" ON "InfrastructureProject"("status");
|
||||
CREATE INDEX "InfrastructureProject_province_category_idx" ON "InfrastructureProject"("province", "category");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Report" ADD CONSTRAINT "Report_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import bcrypt from 'bcrypt';
|
||||
import pg from 'pg';
|
||||
import { importMarketData } from '../scripts/import-market-data';
|
||||
import { seedIndustrialParks } from '../scripts/seed-industrial-parks';
|
||||
import { seedPlans } from '../scripts/seed-plans';
|
||||
import { seedPOIs } from '../scripts/seed-pois';
|
||||
|
||||
@@ -748,7 +749,11 @@ async function main() {
|
||||
await seedPOIs(prisma);
|
||||
console.log('');
|
||||
|
||||
// Phase 11 — Market Data
|
||||
// Phase 11 — Industrial Parks (KCN)
|
||||
await seedIndustrialParks();
|
||||
console.log('');
|
||||
|
||||
// Phase 12 — Market Data
|
||||
await importMarketData();
|
||||
|
||||
console.log('\n' + '━'.repeat(60));
|
||||
@@ -775,6 +780,7 @@ async function main() {
|
||||
console.log(' Saved Searches: 4');
|
||||
console.log(' Notifications: 10 + 6 prefs');
|
||||
console.log(' Audit Logs: 5');
|
||||
console.log(' Industrial: 20 parks');
|
||||
console.log(' Market Index: ~240 records');
|
||||
console.log('\n🔐 Admin Login:');
|
||||
console.log(' Phone: 0876677771');
|
||||
|
||||
Reference in New Issue
Block a user