Files
goodgo-platform/scripts/seed-districts.ts
Ho Ngoc Hai 0c227b6b01 fix: resolve typecheck errors in seed scripts
Add non-null assertions for array indexing in prisma/seed.ts and
scripts/seed-districts.ts to satisfy strict TypeScript checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 12:45:17 +07:00

324 lines
12 KiB
TypeScript

/**
* Seed Vietnam district/ward data for development.
*
* Creates sample properties across major cities (HCM, Hanoi, Da Nang)
* to ensure district coverage in the dev database.
*
* Usage: npx tsx scripts/seed-districts.ts
* Idempotent: safe to run multiple times.
*/
import { PrismaClient, PropertyType, Direction } from '@prisma/client';
const prisma = new PrismaClient();
// =============================================================================
// District & Ward data — canonical source
// =============================================================================
export interface DistrictData {
district: string;
wards: string[];
}
export const HCM_DISTRICTS: DistrictData[] = [
{
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',
],
},
];
export const HANOI_DISTRICTS: DistrictData[] = [
{
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',
],
},
];
export const DANANG_DISTRICTS: DistrictData[] = [
{
district: 'Hải Châu',
wards: [
'Hải Châu 1', 'Hải Châu 2', 'Thạch Thang', 'Thanh Bình', 'Thuận Phước',
'Hòa Thuận Tây', 'Hòa Thuận Đông', 'Nam Dương', 'Phước Ninh',
'Bình Hiên', 'Bình Thuận', 'Hòa Cường Bắc', 'Hòa Cường Nam',
],
},
{
district: 'Thanh Khê',
wards: [
'Tam Thuận', 'Thanh Khê Đông', 'Thanh Khê Tây', 'Xuân Hà', 'Tân Chính',
'Chính Gián', 'Vĩnh Trung', 'Thạc Gián', 'An Khê', 'Hòa Khê',
],
},
{
district: 'Sơn Trà',
wards: [
'An Hải Bắc', 'An Hải Đông', 'An Hải Tây', 'Mân Thái', 'Nại Hiên Đông',
'Phước Mỹ', 'Thọ Quang',
],
},
{
district: 'Ngũ Hành Sơn',
wards: [
'Hòa Hải', 'Hòa Quý', 'Khuê Mỹ', 'Mỹ An',
],
},
{
district: 'Liên Chiểu',
wards: [
'Hòa Hiệp Bắc', 'Hòa Hiệp Nam', 'Hòa Khánh Bắc', 'Hòa Khánh Nam',
'Hòa Minh',
],
},
{
district: 'Cẩm Lệ',
wards: [
'Hòa An', 'Hòa Phát', 'Hòa Thọ Đông', 'Hòa Thọ Tây', 'Hòa Xuân',
'Khuê Trung',
],
},
];
// Coordinates for property generation
export const CITY_COORDINATES: Record<string, Record<string, { lat: number; lng: number }>> = {
'Hồ Chí Minh': {
'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 },
},
'Hà Nội': {
'Hoàn Kiếm': { lat: 21.0285, lng: 105.8542 },
'Ba Đình': { lat: 21.0355, lng: 105.8193 },
'Đống Đa': { lat: 21.0155, lng: 105.8282 },
'Hai Bà Trưng': { lat: 21.0064, lng: 105.8594 },
'Cầu Giấy': { lat: 21.0313, lng: 105.7977 },
'Tây Hồ': { lat: 21.0645, lng: 105.8237 },
'Nam Từ Liêm': { lat: 21.0175, lng: 105.7588 },
},
'Đà Nẵng': {
'Hải Châu': { lat: 16.0544, lng: 108.2022 },
'Thanh Khê': { lat: 16.0674, lng: 108.1811 },
'Sơn Trà': { lat: 16.0894, lng: 108.2331 },
'Ngũ Hành Sơn': { lat: 16.0211, lng: 108.2462 },
'Liên Chiểu': { lat: 16.0777, lng: 108.1478 },
'Cẩm Lệ': { lat: 16.0186, lng: 108.2012 },
},
};
export function getAllDistricts(): { city: string; districts: DistrictData[] }[] {
return [
{ city: 'Hồ Chí Minh', districts: HCM_DISTRICTS },
{ city: 'Hà Nội', districts: HANOI_DISTRICTS },
{ city: 'Đà Nẵng', districts: DANANG_DISTRICTS },
];
}
// =============================================================================
// Seed: create sample properties across all districts
// =============================================================================
const PROPERTY_TEMPLATES = [
{ type: PropertyType.APARTMENT, titleFn: (d: string) => `Căn hộ cao cấp ${d}`, area: 75, beds: 2, baths: 2, dir: Direction.SOUTHEAST },
{ type: PropertyType.TOWNHOUSE, titleFn: (d: string) => `Nhà phố ${d} 1 trệt 2 lầu`, area: 120, beds: 3, baths: 3, dir: Direction.SOUTH },
{ type: PropertyType.LAND, titleFn: (d: string) => `Đất nền thổ cư ${d}`, area: 100, beds: null, baths: null, dir: Direction.EAST },
];
async function seedDistrictProperties() {
console.log('Seeding district properties across HCM, Hanoi, Da Nang...\n');
let created = 0;
let skipped = 0;
for (const { city, districts } of getAllDistricts()) {
console.log(` ${city}:`);
const coords = CITY_COORDINATES[city] ?? {};
for (const { district, wards } of districts) {
const ward = wards[0];
const template = PROPERTY_TEMPLATES[created % PROPERTY_TEMPLATES.length]!;
const loc = coords[district] ?? { lat: 10.0, lng: 106.0 };
// Small random offset so each property has unique coords
const jitterLat = (Math.random() - 0.5) * 0.005;
const jitterLng = (Math.random() - 0.5) * 0.005;
const propId = `dist-prop-${city.substring(0, 3).toLowerCase()}-${district.substring(0, 10).toLowerCase().replace(/\s/g, '-')}`;
try {
await prisma.$executeRaw`
INSERT INTO "Property" (
"id", "propertyType", "title", "description", "address",
"ward", "district", "city", "location",
"areaM2", "bedrooms", "bathrooms", "direction",
"legalStatus", "createdAt", "updatedAt"
) VALUES (
${propId}, ${template.type}::"PropertyType",
${template.titleFn(district)},
${'Bất động sản mẫu dùng cho phát triển. ' + district + ', ' + city + '.'},
${'Số 1 Đường chính'},
${ward}, ${district}, ${city},
ST_SetSRID(ST_MakePoint(${loc.lng + jitterLng}, ${loc.lat + jitterLat}), 4326),
${template.area}, ${template.beds}, ${template.baths},
${template.dir}::"Direction",
${'Sổ hồng'}, NOW(), NOW()
)
ON CONFLICT ("id") DO NOTHING
`;
created++;
} catch {
skipped++;
}
}
const cityDistricts = districts.length;
console.log(` ${cityDistricts} districts processed`);
}
console.log(`\n Total: ${created} properties created, ${skipped} skipped (already exist)`);
}
async function main() {
console.log('=== Seed Districts — Vietnam Real Estate Dev Data ===\n');
// Log summary
for (const { city, districts } of getAllDistricts()) {
const totalWards = districts.reduce((sum, d) => sum + d.wards.length, 0);
console.log(` ${city}: ${districts.length} districts, ${totalWards} wards`);
}
console.log('');
await seedDistrictProperties();
console.log('\nDone.');
}
// Run standalone or import as module
if (require.main === module) {
main()
.catch((e) => {
console.error('Seed districts failed:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
}