feat(seed): add standalone seed scripts for districts, plans, and market data
- scripts/seed-districts.ts: Vietnam district/ward data for HCM, Hanoi, Da Nang with sample properties - scripts/seed-plans.ts: Subscription plans (FREE, AGENT_PRO, INVESTOR, ENTERPRISE) - scripts/import-market-data.ts: Market index data across all 3 cities with realistic pricing - All scripts are idempotent (upsert/ON CONFLICT DO NOTHING) - Refactored prisma/seed.ts to import shared data from scripts, removing duplication Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
478
prisma/seed.ts
478
prisma/seed.ts
@@ -1,450 +1,28 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
UserRole,
|
||||
PlanTier,
|
||||
PropertyType,
|
||||
TransactionType,
|
||||
ListingStatus,
|
||||
Direction,
|
||||
} from '@prisma/client';
|
||||
import { PLANS, seedPlans } from '../scripts/seed-plans';
|
||||
import { HCM_DISTRICTS, HANOI_DISTRICTS, DANANG_DISTRICTS, CITY_COORDINATES } from '../scripts/seed-districts';
|
||||
import { importMarketData } from '../scripts/import-market-data';
|
||||
|
||||
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 },
|
||||
};
|
||||
const SAMPLE_LOCATIONS = CITY_COORDINATES['Hồ Chí Minh'];
|
||||
|
||||
// =============================================================================
|
||||
// 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`);
|
||||
}
|
||||
// seedPlans is imported from scripts/seed-plans.ts
|
||||
|
||||
async function seedUsers() {
|
||||
console.log('Seeding sample users...');
|
||||
@@ -717,49 +295,7 @@ async function seedProperties(users: Awaited<ReturnType<typeof seedUsers>>) {
|
||||
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');
|
||||
}
|
||||
// seedMarketIndex is now handled by importMarketData from scripts/import-market-data.ts
|
||||
|
||||
// =============================================================================
|
||||
// Main seed
|
||||
@@ -771,7 +307,7 @@ async function main() {
|
||||
await seedPlans();
|
||||
const users = await seedUsers();
|
||||
await seedProperties(users);
|
||||
await seedMarketIndex();
|
||||
await importMarketData();
|
||||
|
||||
console.log('\n✅ Seed completed successfully!');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user