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:
Ho Ngoc Hai
2026-04-08 05:10:05 +07:00
parent ea10c28539
commit 51c6eed565
4 changed files with 742 additions and 471 deletions

View File

@@ -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!');
}