feat(db): add initial Prisma migration and seed script
- Add init migration with all 23 models including PostGIS - Add seed script for districts, plans, and sample data Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
468
prisma/migrations/20260407165528_init/migration.sql
Normal file
468
prisma/migrations/20260407165528_init/migration.sql
Normal file
@@ -0,0 +1,468 @@
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "postgis";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserRole" AS ENUM ('BUYER', 'SELLER', 'AGENT', 'ADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "KYCStatus" AS ENUM ('NONE', 'PENDING', 'VERIFIED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PropertyType" AS ENUM ('APARTMENT', 'VILLA', 'TOWNHOUSE', 'LAND', 'OFFICE', 'SHOPHOUSE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionType" AS ENUM ('SALE', 'RENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ListingStatus" AS ENUM ('DRAFT', 'PENDING_REVIEW', 'ACTIVE', 'RESERVED', 'SOLD', 'RENTED', 'EXPIRED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Direction" AS ENUM ('NORTH', 'SOUTH', 'EAST', 'WEST', 'NORTHEAST', 'NORTHWEST', 'SOUTHEAST', 'SOUTHWEST');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionStatus" AS ENUM ('INQUIRY', 'VIEWING_SCHEDULED', 'OFFER_MADE', 'DEPOSIT_PAID', 'CONTRACT_SIGNING', 'COMPLETED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentProvider" AS ENUM ('VNPAY', 'MOMO', 'ZALOPAY', 'BANK_TRANSFER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'REFUNDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentType" AS ENUM ('SUBSCRIPTION', 'LISTING_FEE', 'DEPOSIT', 'FEATURED_LISTING');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PlanTier" AS ENUM ('FREE', 'AGENT_PRO', 'INVESTOR', 'ENTERPRISE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'PAST_DUE', 'CANCELLED', 'EXPIRED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"phone" TEXT NOT NULL,
|
||||
"passwordHash" TEXT,
|
||||
"fullName" TEXT NOT NULL,
|
||||
"avatarUrl" TEXT,
|
||||
"role" "UserRole" NOT NULL DEFAULT 'BUYER',
|
||||
"kycStatus" "KYCStatus" NOT NULL DEFAULT 'NONE',
|
||||
"kycData" JSONB,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Agent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"licenseNumber" TEXT,
|
||||
"agency" TEXT,
|
||||
"qualityScore" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"totalDeals" INTEGER NOT NULL DEFAULT 0,
|
||||
"responseTimeAvg" INTEGER,
|
||||
"bio" TEXT,
|
||||
"serviceAreas" JSONB NOT NULL,
|
||||
"isVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Property" (
|
||||
"id" TEXT NOT NULL,
|
||||
"propertyType" "PropertyType" NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"address" TEXT NOT NULL,
|
||||
"ward" TEXT NOT NULL,
|
||||
"district" TEXT NOT NULL,
|
||||
"city" TEXT NOT NULL,
|
||||
"location" geometry(Point, 4326) NOT NULL,
|
||||
"areaM2" DOUBLE PRECISION NOT NULL,
|
||||
"usableAreaM2" DOUBLE PRECISION,
|
||||
"bedrooms" INTEGER,
|
||||
"bathrooms" INTEGER,
|
||||
"floors" INTEGER,
|
||||
"floor" INTEGER,
|
||||
"totalFloors" INTEGER,
|
||||
"direction" "Direction",
|
||||
"yearBuilt" INTEGER,
|
||||
"legalStatus" TEXT,
|
||||
"amenities" JSONB,
|
||||
"nearbyPOIs" JSONB,
|
||||
"metroDistanceM" DOUBLE PRECISION,
|
||||
"projectName" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Property_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PropertyMedia" (
|
||||
"id" TEXT NOT NULL,
|
||||
"propertyId" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"caption" TEXT,
|
||||
"aiTags" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PropertyMedia_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Listing" (
|
||||
"id" TEXT NOT NULL,
|
||||
"propertyId" TEXT NOT NULL,
|
||||
"agentId" TEXT,
|
||||
"sellerId" TEXT NOT NULL,
|
||||
"transactionType" "TransactionType" NOT NULL,
|
||||
"status" "ListingStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"priceVND" BIGINT NOT NULL,
|
||||
"pricePerM2" DOUBLE PRECISION,
|
||||
"rentPriceMonthly" BIGINT,
|
||||
"commissionPct" DOUBLE PRECISION DEFAULT 2.0,
|
||||
"aiPriceEstimate" BIGINT,
|
||||
"aiConfidence" DOUBLE PRECISION,
|
||||
"moderationScore" DOUBLE PRECISION,
|
||||
"moderationNotes" TEXT,
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"saveCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"inquiryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"featuredUntil" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Listing_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SavedSearch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"filters" JSONB NOT NULL,
|
||||
"alertEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastAlertAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SavedSearch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Transaction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"listingId" TEXT NOT NULL,
|
||||
"buyerId" TEXT NOT NULL,
|
||||
"status" "TransactionStatus" NOT NULL DEFAULT 'INQUIRY',
|
||||
"agreedPrice" BIGINT,
|
||||
"depositAmount" BIGINT,
|
||||
"timeline" JSONB,
|
||||
"contractUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Inquiry" (
|
||||
"id" TEXT NOT NULL,
|
||||
"listingId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"phone" TEXT,
|
||||
"isRead" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Inquiry_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Lead" (
|
||||
"id" TEXT NOT NULL,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"source" TEXT NOT NULL,
|
||||
"score" DOUBLE PRECISION,
|
||||
"notes" JSONB,
|
||||
"status" TEXT NOT NULL DEFAULT 'new',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Lead_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Payment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"transactionId" TEXT,
|
||||
"provider" "PaymentProvider" NOT NULL,
|
||||
"type" "PaymentType" NOT NULL,
|
||||
"amountVND" BIGINT NOT NULL,
|
||||
"status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"providerTxId" TEXT,
|
||||
"callbackData" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Plan" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tier" "PlanTier" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"priceMonthlyVND" BIGINT NOT NULL,
|
||||
"priceYearlyVND" BIGINT NOT NULL,
|
||||
"maxListings" INTEGER,
|
||||
"maxSavedSearches" INTEGER,
|
||||
"features" JSONB NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Plan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"planId" TEXT NOT NULL,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"currentPeriodStart" TIMESTAMP(3) NOT NULL,
|
||||
"currentPeriodEnd" TIMESTAMP(3) NOT NULL,
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UsageRecord" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"metric" TEXT NOT NULL,
|
||||
"count" INTEGER NOT NULL,
|
||||
"periodStart" TIMESTAMP(3) NOT NULL,
|
||||
"periodEnd" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UsageRecord_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Valuation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"propertyId" TEXT NOT NULL,
|
||||
"estimatedPrice" BIGINT NOT NULL,
|
||||
"confidence" DOUBLE PRECISION NOT NULL,
|
||||
"pricePerM2" DOUBLE PRECISION NOT NULL,
|
||||
"comparables" JSONB NOT NULL,
|
||||
"features" JSONB NOT NULL,
|
||||
"modelVersion" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Valuation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "MarketIndex" (
|
||||
"id" TEXT NOT NULL,
|
||||
"district" TEXT NOT NULL,
|
||||
"city" TEXT NOT NULL,
|
||||
"propertyType" "PropertyType" NOT NULL,
|
||||
"period" TEXT NOT NULL,
|
||||
"medianPrice" BIGINT NOT NULL,
|
||||
"avgPriceM2" DOUBLE PRECISION NOT NULL,
|
||||
"totalListings" INTEGER NOT NULL,
|
||||
"daysOnMarket" DOUBLE PRECISION NOT NULL,
|
||||
"inventoryLevel" INTEGER NOT NULL,
|
||||
"absorptionRate" DOUBLE PRECISION,
|
||||
"yoyChange" DOUBLE PRECISION,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MarketIndex_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Review" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"targetType" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"rating" INTEGER NOT NULL,
|
||||
"comment" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_phone_idx" ON "User"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_role_idx" ON "User"("role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Agent_userId_key" ON "Agent"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Agent_qualityScore_idx" ON "Agent"("qualityScore");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Agent_isVerified_idx" ON "Agent"("isVerified");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Property_propertyType_idx" ON "Property"("propertyType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Property_district_city_idx" ON "Property"("district", "city");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Property_location_idx" ON "Property" USING GIST ("location");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PropertyMedia_propertyId_idx" ON "PropertyMedia"("propertyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Listing_status_idx" ON "Listing"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Listing_transactionType_idx" ON "Listing"("transactionType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Listing_priceVND_idx" ON "Listing"("priceVND");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Listing_agentId_idx" ON "Listing"("agentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Listing_publishedAt_idx" ON "Listing"("publishedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SavedSearch_userId_idx" ON "SavedSearch"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Transaction_listingId_idx" ON "Transaction"("listingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Transaction_buyerId_idx" ON "Transaction"("buyerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Transaction_status_idx" ON "Transaction"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Inquiry_listingId_idx" ON "Inquiry"("listingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Inquiry_userId_idx" ON "Inquiry"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Lead_agentId_idx" ON "Lead"("agentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Lead_status_idx" ON "Lead"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Payment_userId_idx" ON "Payment"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Payment_status_idx" ON "Payment"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Payment_providerTxId_idx" ON "Payment"("providerTxId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Plan_tier_key" ON "Plan"("tier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_status_idx" ON "Subscription"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UsageRecord_subscriptionId_metric_idx" ON "UsageRecord"("subscriptionId", "metric");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Valuation_propertyId_idx" ON "Valuation"("propertyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "MarketIndex_city_period_idx" ON "MarketIndex"("city", "period");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "MarketIndex_district_city_propertyType_period_key" ON "MarketIndex"("district", "city", "propertyType", "period");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Review_targetType_targetId_idx" ON "Review"("targetType", "targetId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PropertyMedia" ADD CONSTRAINT "PropertyMedia_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Listing" ADD CONSTRAINT "Listing_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Listing" ADD CONSTRAINT "Listing_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Listing" ADD CONSTRAINT "Listing_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SavedSearch" ADD CONSTRAINT "SavedSearch_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Inquiry" ADD CONSTRAINT "Inquiry_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Inquiry" ADD CONSTRAINT "Inquiry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_planId_fkey" FOREIGN KEY ("planId") REFERENCES "Plan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UsageRecord" ADD CONSTRAINT "UsageRecord_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Valuation" ADD CONSTRAINT "Valuation_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
786
prisma/seed.ts
Normal file
786
prisma/seed.ts
Normal file
@@ -0,0 +1,786 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
UserRole,
|
||||
PlanTier,
|
||||
PropertyType,
|
||||
TransactionType,
|
||||
ListingStatus,
|
||||
Direction,
|
||||
} from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// =============================================================================
|
||||
// Districts & Wards — HCM & Hanoi
|
||||
// =============================================================================
|
||||
|
||||
const HCM_DISTRICTS = [
|
||||
{
|
||||
district: 'Quận 1',
|
||||
wards: [
|
||||
'Bến Nghé',
|
||||
'Bến Thành',
|
||||
'Cầu Kho',
|
||||
'Cầu Ông Lãnh',
|
||||
'Cô Giang',
|
||||
'Đa Kao',
|
||||
'Nguyễn Cư Trinh',
|
||||
'Nguyễn Thái Bình',
|
||||
'Phạm Ngũ Lão',
|
||||
'Tân Định',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận 3',
|
||||
wards: [
|
||||
'Phường 1',
|
||||
'Phường 2',
|
||||
'Phường 3',
|
||||
'Phường 4',
|
||||
'Phường 5',
|
||||
'Phường 9',
|
||||
'Phường 10',
|
||||
'Phường 11',
|
||||
'Phường 12',
|
||||
'Phường 13',
|
||||
'Phường 14',
|
||||
'Võ Thị Sáu',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận 7',
|
||||
wards: [
|
||||
'Bình Thuận',
|
||||
'Phú Mỹ',
|
||||
'Phú Thuận',
|
||||
'Tân Hưng',
|
||||
'Tân Kiểng',
|
||||
'Tân Phong',
|
||||
'Tân Phú',
|
||||
'Tân Quy',
|
||||
'Tân Thuận Đông',
|
||||
'Tân Thuận Tây',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Thủ Đức',
|
||||
wards: [
|
||||
'An Khánh',
|
||||
'An Lợi Đông',
|
||||
'An Phú',
|
||||
'Bình Chiểu',
|
||||
'Bình Thọ',
|
||||
'Bình Trưng Đông',
|
||||
'Bình Trưng Tây',
|
||||
'Cát Lái',
|
||||
'Hiệp Bình Chánh',
|
||||
'Hiệp Bình Phước',
|
||||
'Linh Chiểu',
|
||||
'Linh Đông',
|
||||
'Linh Tây',
|
||||
'Linh Trung',
|
||||
'Linh Xuân',
|
||||
'Long Bình',
|
||||
'Long Phước',
|
||||
'Long Thạnh Mỹ',
|
||||
'Long Trường',
|
||||
'Phú Hữu',
|
||||
'Phước Bình',
|
||||
'Phước Long A',
|
||||
'Phước Long B',
|
||||
'Tam Bình',
|
||||
'Tam Phú',
|
||||
'Tân Phú',
|
||||
'Thạnh Mỹ Lợi',
|
||||
'Thảo Điền',
|
||||
'Thủ Thiêm',
|
||||
'Trường Thạnh',
|
||||
'Trường Thọ',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận Bình Thạnh',
|
||||
wards: [
|
||||
'Phường 1',
|
||||
'Phường 2',
|
||||
'Phường 3',
|
||||
'Phường 5',
|
||||
'Phường 6',
|
||||
'Phường 7',
|
||||
'Phường 11',
|
||||
'Phường 12',
|
||||
'Phường 13',
|
||||
'Phường 14',
|
||||
'Phường 15',
|
||||
'Phường 17',
|
||||
'Phường 19',
|
||||
'Phường 21',
|
||||
'Phường 22',
|
||||
'Phường 24',
|
||||
'Phường 25',
|
||||
'Phường 26',
|
||||
'Phường 27',
|
||||
'Phường 28',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận Phú Nhuận',
|
||||
wards: [
|
||||
'Phường 1',
|
||||
'Phường 2',
|
||||
'Phường 3',
|
||||
'Phường 4',
|
||||
'Phường 5',
|
||||
'Phường 7',
|
||||
'Phường 8',
|
||||
'Phường 9',
|
||||
'Phường 10',
|
||||
'Phường 11',
|
||||
'Phường 13',
|
||||
'Phường 15',
|
||||
'Phường 17',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận Tân Bình',
|
||||
wards: [
|
||||
'Phường 1',
|
||||
'Phường 2',
|
||||
'Phường 3',
|
||||
'Phường 4',
|
||||
'Phường 5',
|
||||
'Phường 6',
|
||||
'Phường 7',
|
||||
'Phường 8',
|
||||
'Phường 9',
|
||||
'Phường 10',
|
||||
'Phường 11',
|
||||
'Phường 12',
|
||||
'Phường 13',
|
||||
'Phường 14',
|
||||
'Phường 15',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Quận Gò Vấp',
|
||||
wards: [
|
||||
'Phường 1',
|
||||
'Phường 3',
|
||||
'Phường 4',
|
||||
'Phường 5',
|
||||
'Phường 6',
|
||||
'Phường 7',
|
||||
'Phường 8',
|
||||
'Phường 9',
|
||||
'Phường 10',
|
||||
'Phường 11',
|
||||
'Phường 12',
|
||||
'Phường 13',
|
||||
'Phường 14',
|
||||
'Phường 15',
|
||||
'Phường 16',
|
||||
'Phường 17',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const HANOI_DISTRICTS = [
|
||||
{
|
||||
district: 'Hoàn Kiếm',
|
||||
wards: [
|
||||
'Chương Dương',
|
||||
'Cửa Đông',
|
||||
'Cửa Nam',
|
||||
'Đồng Xuân',
|
||||
'Hàng Bạc',
|
||||
'Hàng Bài',
|
||||
'Hàng Bồ',
|
||||
'Hàng Bông',
|
||||
'Hàng Buồm',
|
||||
'Hàng Đào',
|
||||
'Hàng Gai',
|
||||
'Hàng Mã',
|
||||
'Hàng Trống',
|
||||
'Lý Thái Tổ',
|
||||
'Phan Chu Trinh',
|
||||
'Phúc Tân',
|
||||
'Tràng Tiền',
|
||||
'Trần Hưng Đạo',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Ba Đình',
|
||||
wards: [
|
||||
'Cống Vị',
|
||||
'Điện Biên',
|
||||
'Đội Cấn',
|
||||
'Giảng Võ',
|
||||
'Kim Mã',
|
||||
'Liễu Giai',
|
||||
'Ngọc Hà',
|
||||
'Ngọc Khánh',
|
||||
'Nguyễn Trung Trực',
|
||||
'Phúc Xá',
|
||||
'Quán Thánh',
|
||||
'Thành Công',
|
||||
'Trúc Bạch',
|
||||
'Vĩnh Phúc',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Đống Đa',
|
||||
wards: [
|
||||
'Cát Linh',
|
||||
'Hàng Bột',
|
||||
'Khâm Thiên',
|
||||
'Khương Thượng',
|
||||
'Kim Liên',
|
||||
'Láng Hạ',
|
||||
'Láng Thượng',
|
||||
'Nam Đồng',
|
||||
'Ngã Tư Sở',
|
||||
'Ô Chợ Dừa',
|
||||
'Phương Liên',
|
||||
'Phương Mai',
|
||||
'Quang Trung',
|
||||
'Quốc Tử Giám',
|
||||
'Thổ Quan',
|
||||
'Trung Liệt',
|
||||
'Trung Phụng',
|
||||
'Trung Tự',
|
||||
'Văn Chương',
|
||||
'Văn Miếu',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Hai Bà Trưng',
|
||||
wards: [
|
||||
'Bách Khoa',
|
||||
'Bạch Đằng',
|
||||
'Bạch Mai',
|
||||
'Bùi Thị Xuân',
|
||||
'Cầu Dền',
|
||||
'Đồng Mác',
|
||||
'Đồng Nhân',
|
||||
'Đồng Tâm',
|
||||
'Lê Đại Hành',
|
||||
'Minh Khai',
|
||||
'Ngô Thì Nhậm',
|
||||
'Nguyễn Du',
|
||||
'Phạm Đình Hổ',
|
||||
'Phố Huế',
|
||||
'Quỳnh Lôi',
|
||||
'Quỳnh Mai',
|
||||
'Thanh Lương',
|
||||
'Thanh Nhàn',
|
||||
'Trương Định',
|
||||
'Vĩnh Tuy',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Cầu Giấy',
|
||||
wards: [
|
||||
'Dịch Vọng',
|
||||
'Dịch Vọng Hậu',
|
||||
'Mai Dịch',
|
||||
'Nghĩa Đô',
|
||||
'Nghĩa Tân',
|
||||
'Quan Hoa',
|
||||
'Trung Hòa',
|
||||
'Yên Hòa',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Tây Hồ',
|
||||
wards: [
|
||||
'Bưởi',
|
||||
'Nhật Tân',
|
||||
'Phú Thượng',
|
||||
'Quảng An',
|
||||
'Thụy Khuê',
|
||||
'Tứ Liên',
|
||||
'Xuân La',
|
||||
'Yên Phụ',
|
||||
],
|
||||
},
|
||||
{
|
||||
district: 'Nam Từ Liêm',
|
||||
wards: [
|
||||
'Cầu Diễn',
|
||||
'Đại Mỗ',
|
||||
'Mễ Trì',
|
||||
'Mỹ Đình 1',
|
||||
'Mỹ Đình 2',
|
||||
'Phú Đô',
|
||||
'Phương Canh',
|
||||
'Tây Mỗ',
|
||||
'Trung Văn',
|
||||
'Xuân Phương',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// Subscription Plans
|
||||
// =============================================================================
|
||||
|
||||
const PLANS = [
|
||||
{
|
||||
tier: PlanTier.FREE,
|
||||
name: 'Miễn phí',
|
||||
priceMonthlyVND: BigInt(0),
|
||||
priceYearlyVND: BigInt(0),
|
||||
maxListings: 3,
|
||||
maxSavedSearches: 5,
|
||||
features: {
|
||||
basicSearch: true,
|
||||
listingPost: true,
|
||||
maxPhotos: 5,
|
||||
analytics: false,
|
||||
prioritySupport: false,
|
||||
aiValuation: false,
|
||||
featuredListing: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
tier: PlanTier.AGENT_PRO,
|
||||
name: 'Agent Pro',
|
||||
priceMonthlyVND: BigInt(499_000),
|
||||
priceYearlyVND: BigInt(4_990_000),
|
||||
maxListings: 50,
|
||||
maxSavedSearches: 30,
|
||||
features: {
|
||||
basicSearch: true,
|
||||
listingPost: true,
|
||||
maxPhotos: 30,
|
||||
analytics: true,
|
||||
prioritySupport: true,
|
||||
aiValuation: true,
|
||||
featuredListing: true,
|
||||
leadManagement: true,
|
||||
agentProfile: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tier: PlanTier.INVESTOR,
|
||||
name: 'Investor',
|
||||
priceMonthlyVND: BigInt(999_000),
|
||||
priceYearlyVND: BigInt(9_990_000),
|
||||
maxListings: 20,
|
||||
maxSavedSearches: 100,
|
||||
features: {
|
||||
basicSearch: true,
|
||||
listingPost: true,
|
||||
maxPhotos: 15,
|
||||
analytics: true,
|
||||
prioritySupport: true,
|
||||
aiValuation: true,
|
||||
featuredListing: false,
|
||||
marketReports: true,
|
||||
priceAlerts: true,
|
||||
portfolioTracking: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tier: PlanTier.ENTERPRISE,
|
||||
name: 'Enterprise',
|
||||
priceMonthlyVND: BigInt(4_990_000),
|
||||
priceYearlyVND: BigInt(49_900_000),
|
||||
maxListings: null,
|
||||
maxSavedSearches: null,
|
||||
features: {
|
||||
basicSearch: true,
|
||||
listingPost: true,
|
||||
maxPhotos: 100,
|
||||
analytics: true,
|
||||
prioritySupport: true,
|
||||
aiValuation: true,
|
||||
featuredListing: true,
|
||||
leadManagement: true,
|
||||
agentProfile: true,
|
||||
marketReports: true,
|
||||
priceAlerts: true,
|
||||
portfolioTracking: true,
|
||||
apiAccess: true,
|
||||
whiteLabel: true,
|
||||
dedicatedSupport: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// Sample coordinates for HCM districts
|
||||
// =============================================================================
|
||||
|
||||
const SAMPLE_LOCATIONS: Record<string, { lat: number; lng: number }> = {
|
||||
'Quận 1': { lat: 10.7769, lng: 106.7009 },
|
||||
'Quận 3': { lat: 10.7834, lng: 106.6867 },
|
||||
'Quận 7': { lat: 10.734, lng: 106.7218 },
|
||||
'Thủ Đức': { lat: 10.8544, lng: 106.7536 },
|
||||
'Quận Bình Thạnh': { lat: 10.8065, lng: 106.7098 },
|
||||
'Quận Phú Nhuận': { lat: 10.7993, lng: 106.6815 },
|
||||
'Quận Tân Bình': { lat: 10.8016, lng: 106.6525 },
|
||||
'Quận Gò Vấp': { lat: 10.8384, lng: 106.6652 },
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Seed functions
|
||||
// =============================================================================
|
||||
|
||||
async function seedPlans() {
|
||||
console.log('Seeding subscription plans...');
|
||||
for (const plan of PLANS) {
|
||||
await prisma.plan.upsert({
|
||||
where: { tier: plan.tier },
|
||||
update: {
|
||||
name: plan.name,
|
||||
priceMonthlyVND: plan.priceMonthlyVND,
|
||||
priceYearlyVND: plan.priceYearlyVND,
|
||||
maxListings: plan.maxListings,
|
||||
maxSavedSearches: plan.maxSavedSearches,
|
||||
features: plan.features,
|
||||
},
|
||||
create: plan,
|
||||
});
|
||||
}
|
||||
console.log(` ✓ ${PLANS.length} plans seeded`);
|
||||
}
|
||||
|
||||
async function seedUsers() {
|
||||
console.log('Seeding sample users...');
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { phone: '0900000001' },
|
||||
update: {},
|
||||
create: {
|
||||
phone: '0900000001',
|
||||
email: 'admin@goodgo.vn',
|
||||
fullName: 'Admin GoodGo',
|
||||
role: UserRole.ADMIN,
|
||||
kycStatus: 'VERIFIED',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const agent1 = await prisma.user.upsert({
|
||||
where: { phone: '0900000002' },
|
||||
update: {},
|
||||
create: {
|
||||
phone: '0900000002',
|
||||
email: 'agent.nguyen@goodgo.vn',
|
||||
fullName: 'Nguyễn Văn An',
|
||||
role: UserRole.AGENT,
|
||||
kycStatus: 'VERIFIED',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const agent2 = await prisma.user.upsert({
|
||||
where: { phone: '0900000003' },
|
||||
update: {},
|
||||
create: {
|
||||
phone: '0900000003',
|
||||
email: 'agent.tran@goodgo.vn',
|
||||
fullName: 'Trần Thị Bình',
|
||||
role: UserRole.AGENT,
|
||||
kycStatus: 'VERIFIED',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const buyer = await prisma.user.upsert({
|
||||
where: { phone: '0900000004' },
|
||||
update: {},
|
||||
create: {
|
||||
phone: '0900000004',
|
||||
email: 'buyer.le@gmail.com',
|
||||
fullName: 'Lê Minh Cường',
|
||||
role: UserRole.BUYER,
|
||||
kycStatus: 'NONE',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const seller = await prisma.user.upsert({
|
||||
where: { phone: '0900000005' },
|
||||
update: {},
|
||||
create: {
|
||||
phone: '0900000005',
|
||||
email: 'seller.pham@gmail.com',
|
||||
fullName: 'Phạm Đức Dũng',
|
||||
role: UserRole.SELLER,
|
||||
kycStatus: 'VERIFIED',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create agent profiles
|
||||
await prisma.agent.upsert({
|
||||
where: { userId: agent1.id },
|
||||
update: {},
|
||||
create: {
|
||||
userId: agent1.id,
|
||||
licenseNumber: 'BDS-2024-001',
|
||||
agency: 'GoodGo Premium Realty',
|
||||
qualityScore: 4.8,
|
||||
totalDeals: 127,
|
||||
responseTimeAvg: 15,
|
||||
bio: 'Chuyên viên bất động sản cao cấp Quận 1, Quận 7 với 10 năm kinh nghiệm.',
|
||||
serviceAreas: ['quan-1', 'quan-7', 'thu-duc'],
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.agent.upsert({
|
||||
where: { userId: agent2.id },
|
||||
update: {},
|
||||
create: {
|
||||
userId: agent2.id,
|
||||
licenseNumber: 'BDS-2024-002',
|
||||
agency: 'Saigon Homes',
|
||||
qualityScore: 4.5,
|
||||
totalDeals: 89,
|
||||
responseTimeAvg: 20,
|
||||
bio: 'Chuyên gia bất động sản Bình Thạnh, Phú Nhuận. Tư vấn miễn phí.',
|
||||
serviceAreas: ['binh-thanh', 'phu-nhuan', 'go-vap'],
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(' ✓ 5 users + 2 agent profiles seeded');
|
||||
return { admin, agent1, agent2, buyer, seller };
|
||||
}
|
||||
|
||||
async function seedProperties(users: Awaited<ReturnType<typeof seedUsers>>) {
|
||||
console.log('Seeding sample properties and listings...');
|
||||
|
||||
const sampleProperties = [
|
||||
{
|
||||
propertyType: PropertyType.APARTMENT,
|
||||
title: 'Căn hộ Vinhomes Central Park 3PN view sông',
|
||||
description:
|
||||
'Căn hộ 3 phòng ngủ tại Vinhomes Central Park, tầng cao view sông Sài Gòn. Full nội thất cao cấp, tiện ích đầy đủ.',
|
||||
address: '208 Nguyễn Hữu Cảnh',
|
||||
ward: 'Phường 22',
|
||||
district: 'Quận Bình Thạnh',
|
||||
city: 'Hồ Chí Minh',
|
||||
lat: 10.7942,
|
||||
lng: 106.7214,
|
||||
areaM2: 108,
|
||||
usableAreaM2: 95,
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
floor: 25,
|
||||
totalFloors: 50,
|
||||
direction: Direction.SOUTHEAST,
|
||||
yearBuilt: 2018,
|
||||
legalStatus: 'Sổ hồng',
|
||||
priceVND: BigInt(8_500_000_000),
|
||||
transactionType: TransactionType.SALE,
|
||||
projectName: 'Vinhomes Central Park',
|
||||
},
|
||||
{
|
||||
propertyType: PropertyType.APARTMENT,
|
||||
title: 'Căn hộ The Sun Avenue 2PN cho thuê',
|
||||
description: 'Cho thuê căn hộ 2PN The Sun Avenue, nội thất đầy đủ, gần Metro, view đẹp.',
|
||||
address: '28 Mai Chí Thọ',
|
||||
ward: 'An Phú',
|
||||
district: 'Thủ Đức',
|
||||
city: 'Hồ Chí Minh',
|
||||
lat: 10.7696,
|
||||
lng: 106.7511,
|
||||
areaM2: 76,
|
||||
usableAreaM2: 68,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floor: 15,
|
||||
totalFloors: 28,
|
||||
direction: Direction.NORTH,
|
||||
yearBuilt: 2020,
|
||||
legalStatus: 'Sổ hồng',
|
||||
priceVND: BigInt(15_000_000),
|
||||
transactionType: TransactionType.RENT,
|
||||
projectName: 'The Sun Avenue',
|
||||
},
|
||||
{
|
||||
propertyType: PropertyType.TOWNHOUSE,
|
||||
title: 'Nhà phố Thảo Điền 1 trệt 3 lầu',
|
||||
description:
|
||||
'Nhà phố khu compound an ninh Thảo Điền, 1 trệt 3 lầu, sân vườn rộng, gara ô tô.',
|
||||
address: '12 Nguyễn Văn Hưởng',
|
||||
ward: 'Thảo Điền',
|
||||
district: 'Thủ Đức',
|
||||
city: 'Hồ Chí Minh',
|
||||
lat: 10.8033,
|
||||
lng: 106.7391,
|
||||
areaM2: 200,
|
||||
usableAreaM2: 350,
|
||||
bedrooms: 4,
|
||||
bathrooms: 5,
|
||||
floors: 4,
|
||||
direction: Direction.SOUTH,
|
||||
yearBuilt: 2015,
|
||||
legalStatus: 'Sổ hồng',
|
||||
priceVND: BigInt(25_000_000_000),
|
||||
transactionType: TransactionType.SALE,
|
||||
projectName: null,
|
||||
},
|
||||
{
|
||||
propertyType: PropertyType.LAND,
|
||||
title: 'Đất nền Quận 7 gần Phú Mỹ Hưng',
|
||||
description: 'Đất nền thổ cư 100%, sổ riêng, hẻm ô tô, gần trung tâm Phú Mỹ Hưng.',
|
||||
address: '56 Huỳnh Tấn Phát',
|
||||
ward: 'Phú Thuận',
|
||||
district: 'Quận 7',
|
||||
city: 'Hồ Chí Minh',
|
||||
lat: 10.7312,
|
||||
lng: 106.7283,
|
||||
areaM2: 120,
|
||||
usableAreaM2: null,
|
||||
bedrooms: null,
|
||||
bathrooms: null,
|
||||
direction: Direction.EAST,
|
||||
yearBuilt: null,
|
||||
legalStatus: 'Sổ đỏ',
|
||||
priceVND: BigInt(12_000_000_000),
|
||||
transactionType: TransactionType.SALE,
|
||||
projectName: null,
|
||||
},
|
||||
{
|
||||
propertyType: PropertyType.OFFICE,
|
||||
title: 'Văn phòng cho thuê Quận 1 - 200m²',
|
||||
description:
|
||||
'Văn phòng hạng B tại trung tâm Quận 1, full nội thất, hệ thống PCCC, thang máy.',
|
||||
address: '123 Nguyễn Huệ',
|
||||
ward: 'Bến Nghé',
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
lat: 10.7731,
|
||||
lng: 106.703,
|
||||
areaM2: 200,
|
||||
usableAreaM2: 180,
|
||||
bedrooms: null,
|
||||
bathrooms: 2,
|
||||
floor: 8,
|
||||
totalFloors: 15,
|
||||
direction: Direction.WEST,
|
||||
yearBuilt: 2010,
|
||||
legalStatus: 'Sổ hồng',
|
||||
priceVND: BigInt(80_000_000),
|
||||
transactionType: TransactionType.RENT,
|
||||
projectName: null,
|
||||
},
|
||||
];
|
||||
|
||||
const agents = await prisma.agent.findMany();
|
||||
|
||||
for (let i = 0; i < sampleProperties.length; i++) {
|
||||
const p = sampleProperties[i];
|
||||
const agent = agents[i % agents.length];
|
||||
|
||||
const property = await prisma.$executeRaw`
|
||||
INSERT INTO "Property" (
|
||||
"id", "propertyType", "title", "description", "address",
|
||||
"ward", "district", "city", "location",
|
||||
"areaM2", "usableAreaM2", "bedrooms", "bathrooms",
|
||||
"floors", "floor", "totalFloors", "direction",
|
||||
"yearBuilt", "legalStatus", "amenities", "nearbyPOIs",
|
||||
"metroDistanceM", "projectName", "createdAt", "updatedAt"
|
||||
) VALUES (
|
||||
${`prop-${i + 1}`}, ${p.propertyType}::"PropertyType", ${p.title}, ${p.description}, ${p.address},
|
||||
${p.ward}, ${p.district}, ${p.city}, ST_SetSRID(ST_MakePoint(${p.lng}, ${p.lat}), 4326),
|
||||
${p.areaM2}, ${p.usableAreaM2 ?? null}, ${p.bedrooms ?? null}, ${p.bathrooms ?? null},
|
||||
${p.floors ?? null}, ${p.floor ?? null}, ${p.totalFloors ?? null}, ${p.direction ?? null}::"Direction",
|
||||
${p.yearBuilt ?? null}, ${p.legalStatus ?? null}, ${null}, ${null},
|
||||
${null}, ${p.projectName ?? null}, NOW(), NOW()
|
||||
)
|
||||
ON CONFLICT ("id") DO NOTHING
|
||||
`;
|
||||
|
||||
await prisma.listing.upsert({
|
||||
where: { id: `listing-${i + 1}` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `listing-${i + 1}`,
|
||||
propertyId: `prop-${i + 1}`,
|
||||
agentId: agent.id,
|
||||
sellerId: users.seller.id,
|
||||
transactionType: p.transactionType,
|
||||
status: ListingStatus.ACTIVE,
|
||||
priceVND: p.priceVND,
|
||||
pricePerM2: Number(p.priceVND) / p.areaM2,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(` ✓ ${sampleProperties.length} properties + listings seeded`);
|
||||
}
|
||||
|
||||
async function seedMarketIndex() {
|
||||
console.log('Seeding sample market index data...');
|
||||
|
||||
const districts = ['Quận 1', 'Quận 3', 'Quận 7', 'Thủ Đức', 'Quận Bình Thạnh'];
|
||||
const periods = ['2025-Q4', '2026-Q1'];
|
||||
|
||||
for (const district of districts) {
|
||||
for (const period of periods) {
|
||||
for (const propertyType of [PropertyType.APARTMENT, PropertyType.TOWNHOUSE]) {
|
||||
const basePrice =
|
||||
district === 'Quận 1' ? 120_000_000 : district === 'Quận 7' ? 65_000_000 : 55_000_000;
|
||||
const randomFactor = 0.9 + Math.random() * 0.2;
|
||||
|
||||
await prisma.marketIndex.upsert({
|
||||
where: {
|
||||
district_city_propertyType_period: {
|
||||
district,
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType,
|
||||
period,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
district,
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType,
|
||||
period,
|
||||
medianPrice: BigInt(Math.round(basePrice * randomFactor * 80)),
|
||||
avgPriceM2: basePrice * randomFactor,
|
||||
totalListings: Math.floor(100 + Math.random() * 500),
|
||||
daysOnMarket: 30 + Math.random() * 60,
|
||||
inventoryLevel: Math.floor(50 + Math.random() * 200),
|
||||
absorptionRate: 0.3 + Math.random() * 0.4,
|
||||
yoyChange: -5 + Math.random() * 15,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' ✓ Market index data seeded');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main seed
|
||||
// =============================================================================
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting seed...\n');
|
||||
|
||||
await seedPlans();
|
||||
const users = await seedUsers();
|
||||
await seedProperties(users);
|
||||
await seedMarketIndex();
|
||||
|
||||
console.log('\n✅ Seed completed successfully!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Seed failed:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user