Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 8s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 5s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 41s
Deploy / Build API Image (push) Failing after 6s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 8s
E2E Tests / Playwright E2E (push) Failing after 11s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 49s
Security Scanning / Trivy Scan — Web Image (push) Failing after 31s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 33s
Security Scanning / Trivy Filesystem Scan (push) Failing after 32s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Production DB had 11 User rows with `phoneHash` / `emailHash` either
NULL (legacy seed before the privacy hashing layer) or filled with the
wrong format (an earlier short-circuit used plain SHA-256). Either way,
`PrismaUserRepository.findByPhone` calls
`fieldEncryption.computeHash(phone)` and looks up `phoneHash` —
returning null and surfacing "Số điện thoại hoặc mật khẩu không đúng"
even when the password is correct.
Two fixes:
1. `scripts/backfill-user-pii-hashes.ts` — re-run-safe one-shot:
- Reads `FIELD_ENCRYPTION_KEY` (or `KYC_ENCRYPTION_KEY`),
- Derives the same HMAC key the runtime uses (HKDF-SHA256 with the
"goodgo-field-hash" info string),
- Recomputes `phoneHash` + `emailHash` for every User and writes
them back if they differ from the stored value.
Verified: after run, login of seed-admin-001, seed-agent-001,
seed-buyer-001 and seed-developer-001 all succeed against
api.goodgo.vn with the seed default password.
2. `prisma/seed.ts` — `seedUsers()` now computes the HMAC hashes on
create AND update (idempotent), so future `pnpm db:seed` runs
produce rows that work with the runtime auth flow out of the box.
When `FIELD_ENCRYPTION_KEY` isn't set (dev mode without encryption),
the hash is `null` and the repository falls back to the plaintext
`phone` / `email` query — preserving local-dev behaviour.
Default seed password remains `Velik@2026`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
856 lines
62 KiB
TypeScript
856 lines
62 KiB
TypeScript
/**
|
|
* GoodGo Platform — Comprehensive Seed
|
|
*
|
|
* Seeds ALL 27 models with realistic Vietnamese real estate data.
|
|
* Idempotent: safe to run multiple times (uses upsert + ON CONFLICT).
|
|
*
|
|
* Default admin account:
|
|
* Phone: 0876677771 | Email: hongochai10@icloud.com | Password: Velik@2026
|
|
*/
|
|
|
|
import * as crypto from 'node:crypto';
|
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
import {
|
|
PrismaClient,
|
|
UserRole,
|
|
KYCStatus,
|
|
PropertyType,
|
|
TransactionType,
|
|
ListingStatus,
|
|
Direction,
|
|
TransactionStatus,
|
|
LeadStatus,
|
|
PaymentProvider,
|
|
PaymentStatus,
|
|
PaymentType,
|
|
OrderStatus,
|
|
EscrowStatus,
|
|
SubscriptionStatus,
|
|
PlanTier,
|
|
OAuthProvider,
|
|
NotificationChannel,
|
|
NotificationStatus,
|
|
AdminAction,
|
|
AuditTargetType,
|
|
ProjectDevelopmentStatus,
|
|
} from '@prisma/client';
|
|
import bcrypt from 'bcrypt';
|
|
import pg from 'pg';
|
|
import { importMarketData } from '../scripts/import-market-data';
|
|
import { seedIndustrialListings } from '../scripts/seed-industrial-listings';
|
|
import { seedIndustrialParks } from '../scripts/seed-industrial-parks';
|
|
import { seedMacroAndInfra } from '../scripts/seed-macro-infra';
|
|
import { seedPlans } from '../scripts/seed-plans';
|
|
import { seedPOIs } from '../scripts/seed-pois';
|
|
|
|
const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] });
|
|
const adapter = new PrismaPg(pool);
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
// =============================================================================
|
|
// Constants
|
|
// =============================================================================
|
|
|
|
const DEFAULT_PASSWORD = 'Velik@2026';
|
|
const BCRYPT_ROUNDS = 12;
|
|
|
|
const now = new Date();
|
|
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
const oneMonthLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
const oneYearLater = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
|
|
|
|
// =============================================================================
|
|
// Phase 2 — Users & Identity
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Compute the same `phoneHash` / `emailHash` that the runtime
|
|
* `FieldEncryptionService` writes — HMAC-SHA256 over the lowercased,
|
|
* trimmed value, keyed by HKDF(FIELD_ENCRYPTION_KEY, "goodgo-field-hash").
|
|
* If neither key is set, returns `null` (matches dev-mode behaviour
|
|
* where lookups fall back to plaintext `phone` / `email`).
|
|
*/
|
|
function buildFieldHasher(): (value: string | null | undefined) => string | null {
|
|
const primaryKey =
|
|
process.env['FIELD_ENCRYPTION_KEY'] ?? process.env['KYC_ENCRYPTION_KEY'];
|
|
if (!primaryKey) return () => null;
|
|
const hmacKey = crypto.hkdfSync(
|
|
'sha256',
|
|
Buffer.from(primaryKey, 'hex'),
|
|
Buffer.alloc(0),
|
|
Buffer.from('goodgo-field-hash', 'utf8'),
|
|
32,
|
|
) as unknown as Buffer;
|
|
return (value) => {
|
|
if (!value) return null;
|
|
return crypto
|
|
.createHmac('sha256', hmacKey)
|
|
.update(value.toLowerCase().trim())
|
|
.digest('hex');
|
|
};
|
|
}
|
|
|
|
const computeFieldHash = buildFieldHasher();
|
|
|
|
async function seedUsers(passwordHash: string) {
|
|
console.log('🔐 Seeding users...');
|
|
|
|
const users = [
|
|
{ id: 'seed-admin-001', phone: '+84876677771', email: 'hongochai10@icloud.com', fullName: 'Hồ Ngọc Hải', role: UserRole.ADMIN, kycStatus: KYCStatus.VERIFIED, avatarUrl: 'https://ui-avatars.com/api/?name=Ho+Ngoc+Hai&background=dc2626&color=fff' },
|
|
{ id: 'seed-agent-001', phone: '+84900000002', email: 'agent.nguyen@goodgo.vn', fullName: 'Nguyễn Văn An', role: UserRole.AGENT, kycStatus: KYCStatus.VERIFIED, avatarUrl: 'https://ui-avatars.com/api/?name=Nguyen+Van+An&background=2563eb&color=fff' },
|
|
{ id: 'seed-agent-002', phone: '+84900000003', email: 'agent.tran@goodgo.vn', fullName: 'Trần Thị Bình', role: UserRole.AGENT, kycStatus: KYCStatus.VERIFIED, avatarUrl: 'https://ui-avatars.com/api/?name=Tran+Thi+Binh&background=7c3aed&color=fff' },
|
|
{ id: 'seed-agent-003', phone: '+84900000006', email: 'agent.le.hong@goodgo.vn', fullName: 'Lê Thị Hồng', role: UserRole.AGENT, kycStatus: KYCStatus.VERIFIED, avatarUrl: 'https://ui-avatars.com/api/?name=Le+Thi+Hong&background=059669&color=fff' },
|
|
{ id: 'seed-buyer-001', phone: '+84900000004', email: 'buyer.le@gmail.com', fullName: 'Lê Minh Cường', role: UserRole.BUYER, kycStatus: KYCStatus.NONE, avatarUrl: null },
|
|
{ id: 'seed-buyer-002', phone: '+84900000007', email: 'buyer.hoang@gmail.com', fullName: 'Hoàng Thị Mai', role: UserRole.BUYER, kycStatus: KYCStatus.PENDING, avatarUrl: null },
|
|
{ id: 'seed-seller-001', phone: '+84900000005', email: 'seller.pham@gmail.com', fullName: 'Phạm Đức Dũng', role: UserRole.SELLER, kycStatus: KYCStatus.VERIFIED, avatarUrl: 'https://ui-avatars.com/api/?name=Pham+Duc+Dung&background=d97706&color=fff' },
|
|
{ id: 'seed-seller-002', phone: '+84900000008', email: 'seller.vo@gmail.com', fullName: 'Võ Thanh Tùng', role: UserRole.SELLER, kycStatus: KYCStatus.VERIFIED, avatarUrl: null },
|
|
];
|
|
|
|
for (const u of users) {
|
|
const phoneHash = computeFieldHash(u.phone);
|
|
const emailHash = computeFieldHash(u.email);
|
|
await prisma.user.upsert({
|
|
where: { id: u.id },
|
|
update: {
|
|
passwordHash,
|
|
email: u.email,
|
|
fullName: u.fullName,
|
|
role: u.role,
|
|
kycStatus: u.kycStatus,
|
|
// Keep search-by-phone/email working when re-running seed against
|
|
// a freshly migrated DB or after a key rotation.
|
|
phoneHash,
|
|
emailHash,
|
|
},
|
|
create: {
|
|
id: u.id, phone: u.phone, email: u.email, passwordHash,
|
|
phoneHash, emailHash,
|
|
fullName: u.fullName, role: u.role, kycStatus: u.kycStatus,
|
|
avatarUrl: u.avatarUrl, isActive: true, totpEnabled: false, totpBackupCodes: [],
|
|
},
|
|
});
|
|
}
|
|
|
|
console.log(` ✓ ${users.length} users seeded (all with password: ${DEFAULT_PASSWORD})`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 2b — Agents
|
|
// =============================================================================
|
|
|
|
async function seedAgents() {
|
|
console.log('👤 Seeding agent profiles...');
|
|
|
|
const agents = [
|
|
{ id: 'seed-agentprofile-001', userId: 'seed-agent-001', 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. Tư vấn miễn phí, hỗ trợ pháp lý toàn diện.', serviceAreas: ['quan-1', 'quan-7', 'thu-duc'], isVerified: true },
|
|
{ id: 'seed-agentprofile-002', userId: 'seed-agent-002', 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í, cam kết giá tốt nhất.', serviceAreas: ['binh-thanh', 'phu-nhuan', 'go-vap'], isVerified: true },
|
|
{ id: 'seed-agentprofile-003', userId: 'seed-agent-003', licenseNumber: 'BDS-2024-003', agency: 'Vietnam Land Trust', qualityScore: 4.2, totalDeals: 45, responseTimeAvg: 30, bio: 'Chuyên viên tư vấn đất nền, nhà phố Quận 7 và Nhà Bè. Hỗ trợ vay ngân hàng ưu đãi.', serviceAreas: ['quan-7', 'nha-be', 'binh-chanh'], isVerified: true },
|
|
];
|
|
|
|
for (const a of agents) {
|
|
await prisma.agent.upsert({ where: { userId: a.userId }, update: { qualityScore: a.qualityScore, totalDeals: a.totalDeals }, create: a });
|
|
}
|
|
console.log(` ✓ ${agents.length} agent profiles seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 2c — OAuth Accounts
|
|
// =============================================================================
|
|
|
|
async function seedOAuthAccounts() {
|
|
console.log('🔗 Seeding OAuth accounts...');
|
|
await prisma.oAuthAccount.upsert({
|
|
where: { provider_providerUserId: { provider: OAuthProvider.GOOGLE, providerUserId: 'google-uid-hongochai10' } },
|
|
update: {},
|
|
create: { id: 'seed-oauth-001', userId: 'seed-admin-001', provider: OAuthProvider.GOOGLE, providerUserId: 'google-uid-hongochai10', profile: { name: 'Hồ Ngọc Hải', picture: 'https://lh3.googleusercontent.com/example' } },
|
|
});
|
|
console.log(' ✓ 1 OAuth account seeded');
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 2d — Project Developments
|
|
// =============================================================================
|
|
|
|
async function seedProjectDevelopments() {
|
|
console.log('🏗️ Seeding project developments...');
|
|
|
|
interface ProjectSeed {
|
|
id: string; name: string; slug: string; developer: string;
|
|
developerLogo: string | null; totalUnits: number; completedUnits: number;
|
|
status: ProjectDevelopmentStatus; startDate: string | null; completionDate: string | null;
|
|
description: string; amenities: string; masterPlanUrl: string | null;
|
|
lat: number; lng: number; address: string; ward: string; district: string; city: string;
|
|
minPrice: bigint; maxPrice: bigint; pricePerM2Range: string;
|
|
totalArea: number | null; buildingCount: number | null; floorCount: number | null;
|
|
unitTypes: string; media: string; documents: string | null;
|
|
tags: string[]; isVerified: boolean;
|
|
}
|
|
|
|
const projects: ProjectSeed[] = [
|
|
{
|
|
id: 'seed-project-001', name: 'Vinhomes Grand Park', slug: 'vinhomes-grand-park',
|
|
developer: 'Vingroup', developerLogo: 'https://storage.goodgo.vn/logos/vingroup.png',
|
|
totalUnits: 43000, completedUnits: 38000, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2019-06-01', completionDate: '2024-12-01',
|
|
description: 'Đại đô thị Vinhomes Grand Park tại TP. Thủ Đức, quy mô 271ha với hệ thống tiện ích đẳng cấp gồm công viên 36ha, trung tâm thương mại Vincom Mega Mall, hồ bơi, gym, trường học Vinschool.',
|
|
amenities: '["công viên 36ha","Vincom Mega Mall","hồ bơi","gym","Vinschool","Vinmec","sân tennis","BBQ"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/vinhomes-grand-park.jpg',
|
|
lat: 10.8412, lng: 106.8354, address: 'Đường Nguyễn Xiển', ward: 'Long Thạnh Mỹ', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(2_000_000_000), maxPrice: BigInt(15_000_000_000),
|
|
pricePerM2Range: '{"min": 45000000, "max": 85000000}',
|
|
totalArea: 271, buildingCount: 78, floorCount: 35,
|
|
unitTypes: '["studio","1PN","2PN","3PN","penthouse","shophouse","biệt thự"]',
|
|
media: '["https://storage.goodgo.vn/projects/vgp-1.jpg","https://storage.goodgo.vn/projects/vgp-2.jpg"]',
|
|
documents: null, tags: ['mega-project', 'vingroup', 'thu-duc'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-002', name: 'Masteri Thảo Điền', slug: 'masteri-thao-dien',
|
|
developer: 'Masterise Homes', developerLogo: 'https://storage.goodgo.vn/logos/masterise.png',
|
|
totalUnits: 1400, completedUnits: 1400, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2014-09-01', completionDate: '2017-06-01',
|
|
description: 'Khu căn hộ cao cấp Masteri Thảo Điền tọa lạc ngay trạm Metro số 1, 2 tháp 45 tầng, view sông Sài Gòn. Tiện ích gồm hồ bơi tràn viền, gym, sky lounge, khu thương mại.',
|
|
amenities: '["hồ bơi tràn viền","gym","sky lounge","khu thương mại","sân chơi trẻ em","BBQ"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/masteri-thao-dien.jpg',
|
|
lat: 10.8025, lng: 106.7415, address: '159 Xa lộ Hà Nội', ward: 'Thảo Điền', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(3_500_000_000), maxPrice: BigInt(12_000_000_000),
|
|
pricePerM2Range: '{"min": 65000000, "max": 100000000}',
|
|
totalArea: 5.5, buildingCount: 2, floorCount: 45,
|
|
unitTypes: '["1PN","2PN","3PN","penthouse"]',
|
|
media: '["https://storage.goodgo.vn/projects/mtd-1.jpg","https://storage.goodgo.vn/projects/mtd-2.jpg"]',
|
|
documents: null, tags: ['luxury', 'metro', 'thao-dien'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-003', name: 'The Metropole Thủ Thiêm', slug: 'the-metropole-thu-thiem',
|
|
developer: 'SonKim Land', developerLogo: 'https://storage.goodgo.vn/logos/sonkim.png',
|
|
totalUnits: 1150, completedUnits: 800, status: ProjectDevelopmentStatus.HANDOVER,
|
|
startDate: '2019-03-01', completionDate: '2026-06-01',
|
|
description: 'The Metropole Thủ Thiêm — dự án hạng sang tại bán đảo Thủ Thiêm, 4 giai đoạn phát triển. Thiết kế bởi Foster + Partners, nội thất chuẩn Châu Âu, view toàn cảnh sông Sài Gòn.',
|
|
amenities: '["hồ bơi vô cực","sky bar","gym","spa","vườn Nhật","tennis","khu BBQ"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/metropole-thu-thiem.jpg',
|
|
lat: 10.7842, lng: 106.7211, address: 'Đại lộ Mai Chí Thọ', ward: 'An Khánh', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(8_000_000_000), maxPrice: BigInt(50_000_000_000),
|
|
pricePerM2Range: '{"min": 120000000, "max": 250000000}',
|
|
totalArea: 7.6, buildingCount: 4, floorCount: 32,
|
|
unitTypes: '["1PN","2PN","3PN","penthouse","duplex"]',
|
|
media: '["https://storage.goodgo.vn/projects/metro-1.jpg","https://storage.goodgo.vn/projects/metro-2.jpg"]',
|
|
documents: null, tags: ['ultra-luxury', 'thu-thiem', 'waterfront'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-004', name: 'Ecopark', slug: 'ecopark',
|
|
developer: 'Ecopark Group', developerLogo: 'https://storage.goodgo.vn/logos/ecopark.png',
|
|
totalUnits: 25000, completedUnits: 20000, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2012-01-01', completionDate: '2024-01-01',
|
|
description: 'Khu đô thị sinh thái Ecopark rộng 500ha tại Hưng Yên, giáp ranh Hà Nội. Hệ thống hồ cảnh quan, công viên xanh, trường học BIS, bệnh viện, trung tâm thương mại.',
|
|
amenities: '["hồ cảnh quan","công viên","BIS School","bệnh viện","TTTM","sân golf","bể bơi"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/ecopark.jpg',
|
|
lat: 20.9487, lng: 105.9596, address: 'Khu đô thị Ecopark', ward: 'Xuân Quan', district: 'Văn Giang', city: 'Hưng Yên',
|
|
minPrice: BigInt(1_500_000_000), maxPrice: BigInt(30_000_000_000),
|
|
pricePerM2Range: '{"min": 30000000, "max": 80000000}',
|
|
totalArea: 500, buildingCount: 120, floorCount: 40,
|
|
unitTypes: '["studio","1PN","2PN","3PN","biệt thự","liền kề","shophouse"]',
|
|
media: '["https://storage.goodgo.vn/projects/eco-1.jpg","https://storage.goodgo.vn/projects/eco-2.jpg"]',
|
|
documents: null, tags: ['eco-city', 'hanoi-suburb', 'green-living'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-005', name: 'Vinhomes Central Park', slug: 'vinhomes-central-park',
|
|
developer: 'Vingroup', developerLogo: 'https://storage.goodgo.vn/logos/vingroup.png',
|
|
totalUnits: 10000, completedUnits: 10000, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2015-01-01', completionDate: '2018-12-01',
|
|
description: 'Vinhomes Central Park — khu đô thị hạng sang trung tâm Bình Thạnh với Landmark 81 (tòa nhà cao nhất Việt Nam 461m). Công viên ven sông 14ha, Vincom Center, Vinschool, Vinmec.',
|
|
amenities: '["Landmark 81","công viên ven sông 14ha","Vincom Center","hồ bơi","gym","Vinschool","Vinmec"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/vinhomes-central-park.jpg',
|
|
lat: 10.7942, lng: 106.7214, address: '208 Nguyễn Hữu Cảnh', ward: 'Phường 22', district: 'Bình Thạnh', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(3_000_000_000), maxPrice: BigInt(60_000_000_000),
|
|
pricePerM2Range: '{"min": 60000000, "max": 200000000}',
|
|
totalArea: 43.91, buildingCount: 18, floorCount: 81,
|
|
unitTypes: '["studio","1PN","2PN","3PN","4PN","penthouse","biệt thự"]',
|
|
media: '["https://storage.goodgo.vn/projects/vcp-1.jpg","https://storage.goodgo.vn/projects/vcp-2.jpg"]',
|
|
documents: null, tags: ['landmark-81', 'luxury', 'binh-thanh', 'waterfront'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-006', name: 'Sala Đại Quang Minh', slug: 'sala-dai-quang-minh',
|
|
developer: 'Đại Quang Minh', developerLogo: 'https://storage.goodgo.vn/logos/dai-quang-minh.png',
|
|
totalUnits: 3500, completedUnits: 3500, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2015-06-01', completionDate: '2021-01-01',
|
|
description: 'Khu đô thị Sala Đại Quang Minh tại Thủ Thiêm, gồm căn hộ Sadora, Sarimi, Sarina và khu biệt thự. Thiết kế bởi kiến trúc sư Nhật, tiêu chuẩn sống quốc tế.',
|
|
amenities: '["hồ bơi","gym","công viên","trường quốc tế","TTTM","bến du thuyền"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/sala.jpg',
|
|
lat: 10.7721, lng: 106.7432, address: '10 Mai Chí Thọ', ward: 'An Lợi Đông', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(5_000_000_000), maxPrice: BigInt(45_000_000_000),
|
|
pricePerM2Range: '{"min": 80000000, "max": 180000000}',
|
|
totalArea: 10.3, buildingCount: 8, floorCount: 36,
|
|
unitTypes: '["2PN","3PN","penthouse","biệt thự song lập","biệt thự đơn lập"]',
|
|
media: '["https://storage.goodgo.vn/projects/sala-1.jpg","https://storage.goodgo.vn/projects/sala-2.jpg"]',
|
|
documents: null, tags: ['thu-thiem', 'luxury', 'japanese-design'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-007', name: 'Vinhomes Ocean Park', slug: 'vinhomes-ocean-park',
|
|
developer: 'Vingroup', developerLogo: 'https://storage.goodgo.vn/logos/vingroup.png',
|
|
totalUnits: 52000, completedUnits: 48000, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2018-11-01', completionDate: '2024-06-01',
|
|
description: 'Đại đô thị Vinhomes Ocean Park tại Gia Lâm, Hà Nội — 420ha với biển hồ nước mặn Crystal Lagoon 6.1ha, công viên biển, VinWonders, hệ thống tiện ích Vingroup đầy đủ.',
|
|
amenities: '["biển hồ Crystal Lagoon 6.1ha","VinWonders","Vinschool","Vinmec","Vincom","gym","bể bơi"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/ocean-park.jpg',
|
|
lat: 20.9693, lng: 105.9541, address: 'Đường Lý Thái Tông', ward: 'Đa Tốn', district: 'Gia Lâm', city: 'Hà Nội',
|
|
minPrice: BigInt(1_600_000_000), maxPrice: BigInt(20_000_000_000),
|
|
pricePerM2Range: '{"min": 35000000, "max": 75000000}',
|
|
totalArea: 420, buildingCount: 90, floorCount: 35,
|
|
unitTypes: '["studio","1PN","2PN","3PN","penthouse","shophouse","biệt thự","liền kề"]',
|
|
media: '["https://storage.goodgo.vn/projects/vop-1.jpg","https://storage.goodgo.vn/projects/vop-2.jpg"]',
|
|
documents: null, tags: ['mega-project', 'vingroup', 'hanoi', 'beach-city'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-008', name: 'The Global City', slug: 'the-global-city',
|
|
developer: 'Masterise Homes', developerLogo: 'https://storage.goodgo.vn/logos/masterise.png',
|
|
totalUnits: 5000, completedUnits: 0, status: ProjectDevelopmentStatus.UNDER_CONSTRUCTION,
|
|
startDate: '2024-01-01', completionDate: '2028-12-01',
|
|
description: 'The Global City — khu đô thị quốc tế 117.4ha tại An Phú, Thủ Đức. Thiết kế bởi Foster + Partners, với trung tâm thương mại, công viên trung tâm 10ha, shophouse, biệt thự.',
|
|
amenities: '["công viên 10ha","TTTM quốc tế","hồ bơi","gym","trường quốc tế","bệnh viện"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/global-city.jpg',
|
|
lat: 10.7833, lng: 106.7531, address: 'Đường Đỗ Xuân Hợp', ward: 'An Phú', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(15_000_000_000), maxPrice: BigInt(80_000_000_000),
|
|
pricePerM2Range: '{"min": 150000000, "max": 350000000}',
|
|
totalArea: 117.4, buildingCount: 25, floorCount: 45,
|
|
unitTypes: '["căn hộ","shophouse","biệt thự","townhouse"]',
|
|
media: '["https://storage.goodgo.vn/projects/gc-1.jpg","https://storage.goodgo.vn/projects/gc-2.jpg"]',
|
|
documents: null, tags: ['mega-project', 'masterise', 'thu-duc', 'upcoming'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-009', name: 'Phú Mỹ Hưng Midtown', slug: 'phu-my-hung-midtown',
|
|
developer: 'Phú Mỹ Hưng', developerLogo: 'https://storage.goodgo.vn/logos/phu-my-hung.png',
|
|
totalUnits: 2800, completedUnits: 2800, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2017-01-01', completionDate: '2022-06-01',
|
|
description: 'Phú Mỹ Hưng Midtown — tổ hợp căn hộ cao cấp tại trung tâm khu đô thị Phú Mỹ Hưng, Quận 7. Gồm The Grande, The Peak, The Signature với công viên hoa anh đào Sakura Park.',
|
|
amenities: '["Sakura Park","hồ bơi","gym","khu thương mại","playground","tennis"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/pmh-midtown.jpg',
|
|
lat: 10.7285, lng: 106.7195, address: '12 Nguyễn Lương Bằng', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh',
|
|
minPrice: BigInt(4_500_000_000), maxPrice: BigInt(18_000_000_000),
|
|
pricePerM2Range: '{"min": 70000000, "max": 130000000}',
|
|
totalArea: 8.2, buildingCount: 6, floorCount: 30,
|
|
unitTypes: '["1PN","2PN","3PN","penthouse"]',
|
|
media: '["https://storage.goodgo.vn/projects/pmh-1.jpg","https://storage.goodgo.vn/projects/pmh-2.jpg"]',
|
|
documents: null, tags: ['phu-my-hung', 'quan-7', 'sakura-park'], isVerified: true,
|
|
},
|
|
{
|
|
id: 'seed-project-010', name: 'Vinhomes Smart City', slug: 'vinhomes-smart-city',
|
|
developer: 'Vingroup', developerLogo: 'https://storage.goodgo.vn/logos/vingroup.png',
|
|
totalUnits: 45000, completedUnits: 40000, status: ProjectDevelopmentStatus.COMPLETED,
|
|
startDate: '2018-06-01', completionDate: '2024-03-01',
|
|
description: 'Vinhomes Smart City — đại đô thị thông minh đầu tiên tại Việt Nam, 280ha ở Nam Từ Liêm, Hà Nội. Ứng dụng công nghệ IoT, AI trong quản lý vận hành. Sapphire, Ruby, Diamond Alnata.',
|
|
amenities: '["smart home IoT","công viên trung tâm","Vinschool","Vinmec","Vincom","hồ bơi","gym","sân tennis"]',
|
|
masterPlanUrl: 'https://storage.goodgo.vn/masterplans/smart-city.jpg',
|
|
lat: 21.0013, lng: 105.7672, address: 'Đường Lê Trọng Tấn', ward: 'Đại Mỗ', district: 'Nam Từ Liêm', city: 'Hà Nội',
|
|
minPrice: BigInt(1_500_000_000), maxPrice: BigInt(25_000_000_000),
|
|
pricePerM2Range: '{"min": 35000000, "max": 90000000}',
|
|
totalArea: 280, buildingCount: 85, floorCount: 35,
|
|
unitTypes: '["studio","1PN","2PN","3PN","penthouse","shophouse","biệt thự","liền kề"]',
|
|
media: '["https://storage.goodgo.vn/projects/vsc-1.jpg","https://storage.goodgo.vn/projects/vsc-2.jpg"]',
|
|
documents: null, tags: ['smart-city', 'vingroup', 'hanoi', 'iot'], isVerified: true,
|
|
},
|
|
];
|
|
|
|
for (const p of projects) {
|
|
await prisma.$executeRaw`
|
|
INSERT INTO "ProjectDevelopment" (
|
|
"id", "name", "slug", "developer", "developerLogo",
|
|
"totalUnits", "completedUnits", "status", "startDate", "completionDate",
|
|
"description", "amenities", "masterPlanUrl",
|
|
"location", "address", "ward", "district", "city",
|
|
"minPrice", "maxPrice", "pricePerM2Range",
|
|
"totalArea", "buildingCount", "floorCount",
|
|
"unitTypes", "media", "documents", "tags",
|
|
"isVerified", "createdAt", "updatedAt"
|
|
) VALUES (
|
|
${p.id}, ${p.name}, ${p.slug}, ${p.developer}, ${p.developerLogo},
|
|
${p.totalUnits}, ${p.completedUnits}, ${p.status}::"ProjectDevelopmentStatus",
|
|
${p.startDate ? new Date(p.startDate) : null}::timestamp,
|
|
${p.completionDate ? new Date(p.completionDate) : null}::timestamp,
|
|
${p.description}, ${p.amenities}::jsonb, ${p.masterPlanUrl},
|
|
ST_SetSRID(ST_MakePoint(${p.lng}, ${p.lat}), 4326),
|
|
${p.address}, ${p.ward}, ${p.district}, ${p.city},
|
|
${p.minPrice}, ${p.maxPrice}, ${p.pricePerM2Range}::jsonb,
|
|
${p.totalArea}, ${p.buildingCount}, ${p.floorCount},
|
|
${p.unitTypes}::jsonb, ${p.media}::jsonb, ${p.documents ?? null}::jsonb, ${p.tags},
|
|
${p.isVerified}, NOW(), NOW()
|
|
)
|
|
ON CONFLICT ("id") DO NOTHING
|
|
`;
|
|
}
|
|
|
|
console.log(` ✓ ${projects.length} project developments seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 3 — Properties & Media
|
|
// =============================================================================
|
|
|
|
async function seedProperties() {
|
|
console.log('🏠 Seeding properties & media...');
|
|
|
|
interface PropSeed {
|
|
id: string; propertyType: PropertyType; title: string; description: string;
|
|
address: string; ward: string; district: string; city: string;
|
|
lat: number; lng: number; areaM2: number;
|
|
usableAreaM2: number | null; bedrooms: number | null; bathrooms: number | null;
|
|
floors?: number | null; floor?: number | null; totalFloors?: number | null;
|
|
direction: Direction | null; yearBuilt: number | null;
|
|
legalStatus: string | null; projectName: string | null; amenities: string | null;
|
|
}
|
|
|
|
const properties: PropSeed[] = [
|
|
{ id: 'seed-prop-001', propertyType: PropertyType.APARTMENT, title: 'Căn hộ Vinhomes Central Park 3PN view sông Sài Gòn', description: 'Căn hộ 3 phòng ngủ tại Vinhomes Central Park, tầng cao view sông Sài Gòn tuyệt đẹp. Full nội thất cao cấp Châu Âu, tiện ích 5 sao.', 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: 'SO_HONG' as const, projectName: 'Vinhomes Central Park', amenities: '["hồ bơi","gym","công viên","siêu thị"]' },
|
|
{ id: 'seed-prop-002', propertyType: PropertyType.APARTMENT, title: 'Căn hộ The Sun Avenue 2PN cho thuê gần Metro', description: 'Cho thuê căn hộ 2PN The Sun Avenue, nội thất đầy đủ, gần tuyến Metro số 1.', address: '28 Mai Chí Thọ', ward: 'An Phú', district: 'Thành phố 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: 'SO_HONG' as const, projectName: 'The Sun Avenue', amenities: '["hồ bơi","gym","BBQ"]' },
|
|
{ id: 'seed-prop-003', propertyType: PropertyType.TOWNHOUSE, title: 'Nhà phố Thảo Điền 1 trệt 3 lầu compound an ninh', description: 'Nhà phố khu compound an ninh Thảo Điền, sân vườn rộng, gara 2 ô tô.', address: '12 Nguyễn Văn Hưởng', ward: 'Thảo Điền', district: 'Thành phố 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: 'SO_HONG' as const, projectName: null, amenities: '["sân vườn","gara ô tô","bảo vệ 24/7"]' },
|
|
{ id: 'seed-prop-004', propertyType: PropertyType.LAND, title: 'Đất nền Quận 7 gần Phú Mỹ Hưng thổ cư 100%', description: 'Đất nền thổ cư 100%, sổ riêng từng nền, hẻm ô tô 8m.', 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: 'SO_DO' as const, projectName: null, amenities: null },
|
|
{ id: 'seed-prop-005', propertyType: PropertyType.OFFICE, title: 'Văn phòng cho thuê Quận 1 200m² trung tâm Nguyễn Huệ', description: 'Văn phòng hạng B+ trung tâm Quận 1, full nội thất, 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: 'SO_HONG' as const, projectName: null, amenities: '["thang máy","PCCC","bảo vệ 24/7"]' },
|
|
{ id: 'seed-prop-006', propertyType: PropertyType.VILLA, title: 'Biệt thự Sala Đại Quang Minh view công viên', description: 'Biệt thự song lập Sala, 230m² đất, 1 trệt 2 lầu 1 áp mái, bể bơi riêng.', address: '10 Mai Chí Thọ', ward: 'An Lợi Đông', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh', lat: 10.7721, lng: 106.7432, areaM2: 230, usableAreaM2: 420, bedrooms: 5, bathrooms: 6, floors: 3, direction: Direction.NORTHEAST, yearBuilt: 2019, legalStatus: 'SO_HONG' as const, projectName: 'Sala Đại Quang Minh', amenities: '["bể bơi riêng","sân vườn","gara 3 ô tô"]' },
|
|
{ id: 'seed-prop-007', propertyType: PropertyType.SHOPHOUSE, title: 'Shophouse Vạn Phúc City mặt tiền kinh doanh', description: 'Shophouse mặt tiền 30m khu đô thị Vạn Phúc City, 1 trệt 4 lầu.', address: '15 Nguyễn Thị Nhung', ward: 'Hiệp Bình Phước', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh', lat: 10.8345, lng: 106.7188, areaM2: 100, usableAreaM2: 400, bedrooms: 3, bathrooms: 4, floors: 5, direction: Direction.SOUTH, yearBuilt: 2022, legalStatus: 'SO_HONG' as const, projectName: 'Vạn Phúc City', amenities: '["mặt tiền kinh doanh","bãi đỗ xe"]' },
|
|
{ id: 'seed-prop-008', propertyType: PropertyType.APARTMENT, title: 'Căn hộ Masteri Thảo Điền 1PN full nội thất', description: 'Căn hộ 1PN Masteri Thảo Điền, nội thất cao cấp, view hồ bơi.', address: '159 Xa lộ Hà Nội', ward: 'Thảo Điền', district: 'Thành phố Thủ Đức', city: 'Hồ Chí Minh', lat: 10.8025, lng: 106.7415, areaM2: 50, usableAreaM2: 45, bedrooms: 1, bathrooms: 1, floor: 12, totalFloors: 40, direction: Direction.NORTHWEST, yearBuilt: 2017, legalStatus: 'SO_HONG' as const, projectName: 'Masteri Thảo Điền', amenities: '["hồ bơi","gym","sky lounge"]' },
|
|
{ id: 'seed-prop-009', propertyType: PropertyType.APARTMENT, title: 'Căn hộ Midtown Phú Mỹ Hưng 2PN giá tốt', description: 'Căn hộ 2PN Midtown The Peak, Phú Mỹ Hưng. Nội thất cơ bản, view đẹp.', address: '12 Nguyễn Lương Bằng', ward: 'Tân Phú', district: 'Quận 7', city: 'Hồ Chí Minh', lat: 10.7285, lng: 106.7195, areaM2: 85, usableAreaM2: 78, bedrooms: 2, bathrooms: 2, floor: 18, totalFloors: 30, direction: Direction.EAST, yearBuilt: 2021, legalStatus: 'SO_HONG' as const, projectName: 'Midtown Phú Mỹ Hưng', amenities: '["hồ bơi","gym","công viên"]' },
|
|
{ id: 'seed-prop-010', propertyType: PropertyType.TOWNHOUSE, title: 'Nhà phố Gò Vấp 1 trệt 2 lầu sổ hồng riêng', description: 'Nhà phố mới xây tại Gò Vấp, 1 trệt 2 lầu, sân thượng, hẻm xe hơi 6m.', address: '88 Nguyễn Oanh', ward: 'Phường 17', district: 'Quận Gò Vấp', city: 'Hồ Chí Minh', lat: 10.8352, lng: 106.6648, areaM2: 65, usableAreaM2: 150, bedrooms: 3, bathrooms: 3, floors: 3, direction: Direction.WEST, yearBuilt: 2024, legalStatus: 'SO_HONG' as const, projectName: null, amenities: '["sân thượng","ban công"]' },
|
|
];
|
|
|
|
for (const p of properties) {
|
|
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 (
|
|
${p.id}, ${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}, ${p.amenities ?? null}::jsonb, ${null}::jsonb,
|
|
${null}, ${p.projectName ?? null}, NOW(), NOW()
|
|
)
|
|
ON CONFLICT ("id") DO NOTHING
|
|
`;
|
|
}
|
|
|
|
// Property Media — 2 images per property
|
|
for (let i = 0; i < properties.length; i++) {
|
|
const p = properties[i]!;
|
|
for (let j = 0; j < 2; j++) {
|
|
const mediaId = `seed-media-${i * 2 + j + 1}`;
|
|
await prisma.propertyMedia.upsert({
|
|
where: { id: mediaId },
|
|
update: {},
|
|
create: { id: mediaId, propertyId: p.id, url: `https://picsum.photos/seed/${p.id}-${j}/800/600`, type: 'image', order: j, caption: j === 0 ? 'Mặt tiền' : 'Nội thất' },
|
|
});
|
|
}
|
|
}
|
|
|
|
// Link properties to project developments where projectName matches
|
|
const projectLinks: Record<string, string> = {
|
|
'seed-prop-001': 'seed-project-005', // Vinhomes Central Park
|
|
'seed-prop-002': 'seed-project-002', // The Sun Avenue → no match, skip (but Masteri Thao Dien seed-prop-008 matches)
|
|
'seed-prop-006': 'seed-project-006', // Sala Đại Quang Minh
|
|
'seed-prop-008': 'seed-project-002', // Masteri Thảo Điền
|
|
'seed-prop-009': 'seed-project-009', // Midtown Phú Mỹ Hưng
|
|
};
|
|
for (const [propId, projectId] of Object.entries(projectLinks)) {
|
|
await prisma.property.update({ where: { id: propId }, data: { projectDevelopmentId: projectId } });
|
|
}
|
|
|
|
console.log(` ✓ ${properties.length} properties + ${properties.length * 2} media seeded (${Object.keys(projectLinks).length} linked to projects)`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 4 — Listings
|
|
// =============================================================================
|
|
|
|
async function seedListings() {
|
|
console.log('📋 Seeding listings...');
|
|
|
|
const listings = [
|
|
{ id: 'seed-listing-001', propertyId: 'seed-prop-001', agentId: 'seed-agentprofile-001', sellerId: 'seed-seller-001', transactionType: TransactionType.SALE, status: ListingStatus.ACTIVE, priceVND: BigInt(8_500_000_000), pricePerM2: 78703703.7 },
|
|
{ id: 'seed-listing-002', propertyId: 'seed-prop-002', agentId: 'seed-agentprofile-001', sellerId: 'seed-seller-001', transactionType: TransactionType.RENT, status: ListingStatus.ACTIVE, priceVND: BigInt(15_000_000), pricePerM2: null, rentPriceMonthly: BigInt(15_000_000) },
|
|
{ id: 'seed-listing-003', propertyId: 'seed-prop-003', agentId: 'seed-agentprofile-002', sellerId: 'seed-seller-001', transactionType: TransactionType.SALE, status: ListingStatus.ACTIVE, priceVND: BigInt(25_000_000_000), pricePerM2: 125000000.0 },
|
|
{ id: 'seed-listing-004', propertyId: 'seed-prop-004', agentId: 'seed-agentprofile-003', sellerId: 'seed-seller-002', transactionType: TransactionType.SALE, status: ListingStatus.ACTIVE, priceVND: BigInt(12_000_000_000), pricePerM2: 100000000.0 },
|
|
{ id: 'seed-listing-005', propertyId: 'seed-prop-005', agentId: 'seed-agentprofile-001', sellerId: 'seed-seller-001', transactionType: TransactionType.RENT, status: ListingStatus.ACTIVE, priceVND: BigInt(80_000_000), pricePerM2: null, rentPriceMonthly: BigInt(80_000_000) },
|
|
{ id: 'seed-listing-006', propertyId: 'seed-prop-006', agentId: 'seed-agentprofile-002', sellerId: 'seed-seller-002', transactionType: TransactionType.SALE, status: ListingStatus.SOLD, priceVND: BigInt(35_000_000_000), pricePerM2: 152173913.0 },
|
|
{ id: 'seed-listing-007', propertyId: 'seed-prop-007', agentId: 'seed-agentprofile-003', sellerId: 'seed-seller-002', transactionType: TransactionType.SALE, status: ListingStatus.PENDING_REVIEW, priceVND: BigInt(18_000_000_000), pricePerM2: 180000000.0 },
|
|
{ id: 'seed-listing-008', propertyId: 'seed-prop-008', agentId: 'seed-agentprofile-001', sellerId: 'seed-seller-001', transactionType: TransactionType.RENT, status: ListingStatus.ACTIVE, priceVND: BigInt(12_000_000), pricePerM2: null, rentPriceMonthly: BigInt(12_000_000) },
|
|
{ id: 'seed-listing-009', propertyId: 'seed-prop-009', agentId: 'seed-agentprofile-002', sellerId: 'seed-seller-001', transactionType: TransactionType.RENT, status: ListingStatus.ACTIVE, priceVND: BigInt(20_000_000), pricePerM2: null, rentPriceMonthly: BigInt(20_000_000) },
|
|
{ id: 'seed-listing-010', propertyId: 'seed-prop-010', agentId: 'seed-agentprofile-003', sellerId: 'seed-seller-002', transactionType: TransactionType.SALE, status: ListingStatus.ACTIVE, priceVND: BigInt(5_200_000_000), pricePerM2: 80000000.0 },
|
|
];
|
|
|
|
for (const l of listings) {
|
|
const isPublished = l.status === ListingStatus.ACTIVE || l.status === ListingStatus.SOLD;
|
|
await prisma.listing.upsert({
|
|
where: { id: l.id },
|
|
update: {},
|
|
create: {
|
|
...l, commissionPct: 2.0,
|
|
viewCount: Math.floor(Math.random() * 500) + 10,
|
|
saveCount: Math.floor(Math.random() * 50),
|
|
inquiryCount: Math.floor(Math.random() * 20),
|
|
publishedAt: isPublished ? oneWeekAgo : null,
|
|
expiresAt: l.status === ListingStatus.ACTIVE ? oneYearLater : null,
|
|
},
|
|
});
|
|
}
|
|
console.log(` ✓ ${listings.length} listings seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 5 — Subscriptions
|
|
// =============================================================================
|
|
|
|
async function seedSubscriptions() {
|
|
console.log('💎 Seeding subscriptions...');
|
|
|
|
const plans = await prisma.plan.findMany();
|
|
const planMap = Object.fromEntries(plans.map(p => [p.tier, p.id]));
|
|
|
|
const subs = [
|
|
{ id: 'seed-sub-001', userId: 'seed-admin-001', tier: PlanTier.ENTERPRISE },
|
|
{ id: 'seed-sub-002', userId: 'seed-agent-001', tier: PlanTier.AGENT_PRO },
|
|
{ id: 'seed-sub-003', userId: 'seed-agent-002', tier: PlanTier.AGENT_PRO },
|
|
{ id: 'seed-sub-004', userId: 'seed-agent-003', tier: PlanTier.AGENT_PRO },
|
|
{ id: 'seed-sub-005', userId: 'seed-buyer-001', tier: PlanTier.FREE },
|
|
{ id: 'seed-sub-006', userId: 'seed-buyer-002', tier: PlanTier.INVESTOR },
|
|
];
|
|
|
|
for (const s of subs) {
|
|
await prisma.subscription.upsert({
|
|
where: { userId: s.userId },
|
|
update: {},
|
|
create: { id: s.id, userId: s.userId, planId: planMap[s.tier]!, status: SubscriptionStatus.ACTIVE, currentPeriodStart: oneMonthAgo, currentPeriodEnd: oneMonthLater },
|
|
});
|
|
}
|
|
|
|
const usageRecords = [
|
|
{ id: 'seed-usage-001', subscriptionId: 'seed-sub-002', metric: 'listings_created', count: 12, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-002', subscriptionId: 'seed-sub-002', metric: 'analytics_queries', count: 45, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-003', subscriptionId: 'seed-sub-002', metric: 'media_uploads', count: 38, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-004', subscriptionId: 'seed-sub-003', metric: 'listings_created', count: 8, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-005', subscriptionId: 'seed-sub-003', metric: 'analytics_queries', count: 22, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-006', subscriptionId: 'seed-sub-004', metric: 'listings_created', count: 5, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-007', subscriptionId: 'seed-sub-005', metric: 'listings_created', count: 2, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
{ id: 'seed-usage-008', subscriptionId: 'seed-sub-006', metric: 'analytics_queries', count: 150, periodStart: oneMonthAgo, periodEnd: oneMonthLater },
|
|
];
|
|
|
|
for (const u of usageRecords) {
|
|
await prisma.usageRecord.upsert({ where: { id: u.id }, update: {}, create: u });
|
|
}
|
|
console.log(` ✓ ${subs.length} subscriptions + ${usageRecords.length} usage records seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 6 — Inquiries, Transactions, Leads
|
|
// =============================================================================
|
|
|
|
async function seedInquiries() {
|
|
console.log('💬 Seeding inquiries...');
|
|
const inquiries = [
|
|
{ id: 'seed-inq-001', listingId: 'seed-listing-001', userId: 'seed-buyer-001', message: 'Cho em hỏi căn hộ này còn không ạ? Em muốn đặt lịch xem nhà cuối tuần.', phone: '+84900000004', isRead: true },
|
|
{ id: 'seed-inq-002', listingId: 'seed-listing-001', userId: 'seed-buyer-002', message: 'Giá cuối là bao nhiêu? Có thương lượng được không ạ?', phone: '+84900000007', isRead: true },
|
|
{ id: 'seed-inq-003', listingId: 'seed-listing-003', userId: 'seed-buyer-001', message: 'Em quan tâm nhà phố Thảo Điền. Cho xem giấy tờ pháp lý.', isRead: false },
|
|
{ id: 'seed-inq-004', listingId: 'seed-listing-004', userId: 'seed-buyer-002', message: 'Đất nền có quy hoạch gì không? Được xây dựng luôn không ạ?', phone: '+84900000007', isRead: false },
|
|
{ id: 'seed-inq-005', listingId: 'seed-listing-005', userId: 'seed-buyer-001', message: 'Văn phòng hợp đồng tối thiểu bao lâu? Miễn phí tháng đầu không?', isRead: true },
|
|
{ id: 'seed-inq-006', listingId: 'seed-listing-008', userId: 'seed-buyer-002', message: 'Masteri cho thuê có bao gồm phí quản lý không ạ?', phone: '+84900000007', isRead: false },
|
|
];
|
|
for (const inq of inquiries) {
|
|
await prisma.inquiry.upsert({ where: { id: inq.id }, update: {}, create: { ...inq, createdAt: threeDaysAgo } });
|
|
}
|
|
console.log(` ✓ ${inquiries.length} inquiries seeded`);
|
|
}
|
|
|
|
async function seedTransactions() {
|
|
console.log('🔄 Seeding transactions...');
|
|
const transactions = [
|
|
{ id: 'seed-tx-001', listingId: 'seed-listing-001', buyerId: 'seed-buyer-001', status: TransactionStatus.VIEWING_SCHEDULED, timeline: { viewingDate: oneWeekAgo.toISOString() } },
|
|
{ id: 'seed-tx-002', listingId: 'seed-listing-003', buyerId: 'seed-buyer-001', status: TransactionStatus.OFFER_MADE, agreedPrice: BigInt(24_000_000_000), timeline: { viewingDate: oneMonthAgo.toISOString(), offerDate: oneWeekAgo.toISOString() } },
|
|
{ id: 'seed-tx-003', listingId: 'seed-listing-006', buyerId: 'seed-buyer-002', status: TransactionStatus.COMPLETED, agreedPrice: BigInt(34_500_000_000), depositAmount: BigInt(3_450_000_000), timeline: { completedDate: threeDaysAgo.toISOString() }, contractUrl: 'https://storage.goodgo.vn/contracts/seed-tx-003.pdf' },
|
|
{ id: 'seed-tx-004', listingId: 'seed-listing-004', buyerId: 'seed-buyer-002', status: TransactionStatus.INQUIRY },
|
|
];
|
|
for (const t of transactions) {
|
|
await prisma.transaction.upsert({ where: { id: t.id }, update: {}, create: t });
|
|
}
|
|
console.log(` ✓ ${transactions.length} transactions seeded`);
|
|
}
|
|
|
|
async function seedLeads() {
|
|
console.log('🎯 Seeding leads...');
|
|
const leads = [
|
|
{ id: 'seed-lead-001', agentId: 'seed-agentprofile-001', name: 'Nguyễn Thanh Long', phone: '+84912345001', email: 'long.nguyen@email.com', source: 'website', score: 0.85, status: LeadStatus.QUALIFIED, notes: { history: ['Quan tâm căn hộ Quận 1', 'Budget 8-10 tỷ'] } },
|
|
{ id: 'seed-lead-002', agentId: 'seed-agentprofile-001', name: 'Trần Minh Đức', phone: '+84912345002', source: 'referral', score: 0.92, status: LeadStatus.NEGOTIATING, notes: { history: ['Muốn mua nhà phố Thảo Điền'] } },
|
|
{ id: 'seed-lead-003', agentId: 'seed-agentprofile-002', name: 'Phạm Hồng Nhung', phone: '+84912345003', email: 'nhung.pham@email.com', source: 'website', score: 0.65, status: LeadStatus.CONTACTED, notes: { history: ['Tìm căn hộ cho thuê Bình Thạnh'] } },
|
|
{ id: 'seed-lead-004', agentId: 'seed-agentprofile-002', name: 'Lê Quang Vinh', phone: '+84912345004', source: 'phone', score: 0.45, status: LeadStatus.NEW, notes: null },
|
|
{ id: 'seed-lead-005', agentId: 'seed-agentprofile-003', name: 'Đỗ Thị Lan', phone: '+84912345005', email: 'lan.do@email.com', source: 'website', score: 0.78, status: LeadStatus.CONVERTED, notes: { history: ['Đã mua đất nền Quận 7'] } },
|
|
{ id: 'seed-lead-006', agentId: 'seed-agentprofile-003', name: 'Bùi Văn Hùng', phone: '+84912345006', source: 'facebook', score: 0.3, status: LeadStatus.LOST, notes: { history: ['Không liên lạc được'] } },
|
|
];
|
|
for (const l of leads) {
|
|
await prisma.lead.upsert({ where: { id: l.id }, update: {}, create: l });
|
|
}
|
|
console.log(` ✓ ${leads.length} leads seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 7 — Orders, Escrow, Payments
|
|
// =============================================================================
|
|
|
|
async function seedOrdersAndPayments() {
|
|
console.log('💳 Seeding orders, escrow & payments...');
|
|
|
|
const orders = [
|
|
{ id: 'seed-order-001', buyerId: 'seed-buyer-002', sellerId: 'seed-seller-002', listingId: 'seed-listing-006', status: OrderStatus.COMPLETED, amountVND: BigInt(34_500_000_000), platformFeeVND: BigInt(1_725_000_000), sellerPayoutVND: BigInt(32_775_000_000) },
|
|
{ id: 'seed-order-002', buyerId: 'seed-buyer-001', sellerId: 'seed-seller-001', listingId: 'seed-listing-001', status: OrderStatus.ESCROW_HELD, amountVND: BigInt(8_500_000_000), platformFeeVND: BigInt(425_000_000), sellerPayoutVND: BigInt(8_075_000_000) },
|
|
{ id: 'seed-order-003', buyerId: 'seed-buyer-001', sellerId: 'seed-seller-002', listingId: 'seed-listing-010', status: OrderStatus.PAYMENT_PENDING, amountVND: BigInt(5_200_000_000), platformFeeVND: BigInt(260_000_000), sellerPayoutVND: BigInt(4_940_000_000) },
|
|
];
|
|
for (const o of orders) {
|
|
await prisma.order.upsert({ where: { id: o.id }, update: {}, create: o });
|
|
}
|
|
|
|
const escrows = [
|
|
{ id: 'seed-escrow-001', orderId: 'seed-order-001', amountVND: BigInt(34_500_000_000), feeVND: BigInt(345_000_000), status: EscrowStatus.RELEASED, heldAt: oneWeekAgo, releasedAt: threeDaysAgo },
|
|
{ id: 'seed-escrow-002', orderId: 'seed-order-002', amountVND: BigInt(8_500_000_000), feeVND: BigInt(85_000_000), status: EscrowStatus.HELD, heldAt: threeDaysAgo },
|
|
];
|
|
for (const e of escrows) {
|
|
await prisma.escrow.upsert({ where: { orderId: e.orderId }, update: {}, create: e });
|
|
}
|
|
|
|
const payments = [
|
|
{ id: 'seed-pay-001', userId: 'seed-buyer-002', orderId: 'seed-order-001', provider: PaymentProvider.VNPAY, type: PaymentType.DEPOSIT, amountVND: BigInt(3_450_000_000), status: PaymentStatus.COMPLETED, providerTxId: 'VNP-20260401-001' },
|
|
{ id: 'seed-pay-002', userId: 'seed-buyer-002', orderId: 'seed-order-001', provider: PaymentProvider.BANK_TRANSFER, type: PaymentType.AUCTION_PAYMENT, amountVND: BigInt(31_050_000_000), status: PaymentStatus.COMPLETED, providerTxId: 'BANK-20260405-001' },
|
|
{ id: 'seed-pay-003', userId: 'seed-buyer-001', orderId: 'seed-order-002', provider: PaymentProvider.MOMO, type: PaymentType.DEPOSIT, amountVND: BigInt(850_000_000), status: PaymentStatus.COMPLETED, providerTxId: 'MOMO-20260410-001' },
|
|
{ id: 'seed-pay-004', userId: 'seed-agent-001', provider: PaymentProvider.ZALOPAY, type: PaymentType.SUBSCRIPTION, amountVND: BigInt(499_000), status: PaymentStatus.COMPLETED, providerTxId: 'ZALO-20260301-001' },
|
|
{ id: 'seed-pay-005', userId: 'seed-agent-002', provider: PaymentProvider.VNPAY, type: PaymentType.SUBSCRIPTION, amountVND: BigInt(499_000), status: PaymentStatus.COMPLETED, providerTxId: 'VNP-20260301-002' },
|
|
{ id: 'seed-pay-006', userId: 'seed-seller-001', provider: PaymentProvider.VNPAY, type: PaymentType.FEATURED_LISTING, amountVND: BigInt(200_000), status: PaymentStatus.PENDING, providerTxId: null },
|
|
];
|
|
for (const p of payments) {
|
|
await prisma.payment.upsert({ where: { id: p.id }, update: {}, create: p });
|
|
}
|
|
console.log(` ✓ ${orders.length} orders + ${escrows.length} escrows + ${payments.length} payments seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 8 — Reviews, Valuations, Saved Searches
|
|
// =============================================================================
|
|
|
|
async function seedReviews() {
|
|
console.log('⭐ Seeding reviews...');
|
|
const reviews = [
|
|
{ id: 'seed-review-001', userId: 'seed-buyer-001', targetType: 'agent', targetId: 'seed-agentprofile-001', rating: 5, comment: 'Anh An tư vấn rất nhiệt tình, chuyên nghiệp!' },
|
|
{ id: 'seed-review-002', userId: 'seed-buyer-002', targetType: 'agent', targetId: 'seed-agentprofile-001', rating: 4, comment: 'Dịch vụ tốt, phản hồi nhanh.' },
|
|
{ id: 'seed-review-003', userId: 'seed-buyer-001', targetType: 'agent', targetId: 'seed-agentprofile-002', rating: 5, comment: 'Chị Bình am hiểu thị trường lắm. Rất hài lòng!' },
|
|
{ id: 'seed-review-004', userId: 'seed-buyer-002', targetType: 'agent', targetId: 'seed-agentprofile-003', rating: 3, comment: 'Tư vấn ổn nhưng phản hồi hơi chậm.' },
|
|
{ id: 'seed-review-005', userId: 'seed-buyer-001', targetType: 'property', targetId: 'seed-prop-001', rating: 5, comment: 'Căn hộ tuyệt vời, view sông rất đẹp.' },
|
|
{ id: 'seed-review-006', userId: 'seed-buyer-002', targetType: 'property', targetId: 'seed-prop-002', rating: 4, comment: 'Vị trí tốt gần Metro, nội thất cơ bản nhưng sạch sẽ.' },
|
|
{ id: 'seed-review-007', userId: 'seed-buyer-001', targetType: 'property', targetId: 'seed-prop-006', rating: 5, comment: 'Biệt thự Sala thực sự đẳng cấp.' },
|
|
{ id: 'seed-review-008', userId: 'seed-buyer-002', targetType: 'seller', targetId: 'seed-seller-001', rating: 4, comment: 'Anh Dũng rất thẳng thắn, giấy tờ rõ ràng.' },
|
|
];
|
|
for (const r of reviews) {
|
|
await prisma.review.upsert({ where: { id: r.id }, update: {}, create: r });
|
|
}
|
|
console.log(` ✓ ${reviews.length} reviews seeded`);
|
|
}
|
|
|
|
async function seedValuations() {
|
|
console.log('📊 Seeding valuations...');
|
|
const valuations = [
|
|
{ id: 'seed-val-001', propertyId: 'seed-prop-001', estimatedPrice: BigInt(8_800_000_000), confidence: 0.92, pricePerM2: 81481481.5, comparables: { ids: ['c1', 'c2', 'c3'], avgPrice: 8600000000 }, features: { location: 0.3, size: 0.2, floor: 0.15, view: 0.2, project: 0.15 }, modelVersion: 'goodgo-avm-v2.1' },
|
|
{ id: 'seed-val-002', propertyId: 'seed-prop-003', estimatedPrice: BigInt(26_000_000_000), confidence: 0.85, pricePerM2: 130000000.0, comparables: { ids: ['c4', 'c5'], avgPrice: 25000000000 }, features: { location: 0.35, size: 0.25, compound: 0.2, condition: 0.2 }, modelVersion: 'goodgo-avm-v2.1' },
|
|
{ id: 'seed-val-003', propertyId: 'seed-prop-004', estimatedPrice: BigInt(11_500_000_000), confidence: 0.78, pricePerM2: 95833333.3, comparables: { ids: ['c6', 'c7', 'c8'], avgPrice: 11200000000 }, features: { location: 0.4, size: 0.3, legal: 0.3 }, modelVersion: 'goodgo-avm-v2.1' },
|
|
{ id: 'seed-val-004', propertyId: 'seed-prop-006', estimatedPrice: BigInt(36_000_000_000), confidence: 0.88, pricePerM2: 156521739.1, comparables: { ids: ['c9', 'c10'], avgPrice: 35000000000 }, features: { location: 0.3, project: 0.25, size: 0.2, amenities: 0.25 }, modelVersion: 'goodgo-avm-v2.1' },
|
|
{ id: 'seed-val-005', propertyId: 'seed-prop-010', estimatedPrice: BigInt(5_500_000_000), confidence: 0.81, pricePerM2: 84615384.6, comparables: { ids: ['c11', 'c12'], avgPrice: 5300000000 }, features: { location: 0.3, size: 0.25, newBuild: 0.25, legal: 0.2 }, modelVersion: 'goodgo-avm-v2.1' },
|
|
];
|
|
for (const v of valuations) {
|
|
await prisma.valuation.upsert({ where: { id: v.id }, update: {}, create: v });
|
|
}
|
|
console.log(` ✓ ${valuations.length} valuations seeded`);
|
|
}
|
|
|
|
async function seedSavedSearches() {
|
|
console.log('🔍 Seeding saved searches...');
|
|
const searches = [
|
|
{ id: 'seed-search-001', userId: 'seed-buyer-001', name: 'Căn hộ Quận 1 dưới 10 tỷ', filters: { district: 'Quận 1', propertyType: 'APARTMENT', priceMax: 10000000000, bedrooms: 2 }, alertEnabled: true },
|
|
{ id: 'seed-search-002', userId: 'seed-buyer-001', name: 'Nhà phố Thủ Đức', filters: { district: 'Thành phố Thủ Đức', propertyType: 'TOWNHOUSE', transactionType: 'SALE' }, alertEnabled: true },
|
|
{ id: 'seed-search-003', userId: 'seed-buyer-002', name: 'Cho thuê gần Metro', filters: { transactionType: 'RENT', priceMax: 20000000, metroDistanceMax: 1000 }, alertEnabled: true },
|
|
{ id: 'seed-search-004', userId: 'seed-buyer-002', name: 'Đất nền Quận 7 đầu tư', filters: { district: 'Quận 7', propertyType: 'LAND', priceMax: 15000000000 }, alertEnabled: false },
|
|
];
|
|
for (const s of searches) {
|
|
await prisma.savedSearch.upsert({ where: { id: s.id }, update: {}, create: s });
|
|
}
|
|
console.log(` ✓ ${searches.length} saved searches seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 9 — Notifications & Audit
|
|
// =============================================================================
|
|
|
|
async function seedNotifications() {
|
|
console.log('🔔 Seeding notifications...');
|
|
const logs = [
|
|
{ id: 'seed-notif-001', userId: 'seed-buyer-001', channel: NotificationChannel.EMAIL, templateKey: 'new_listing_match', subject: 'Có căn hộ phù hợp tiêu chí của bạn', body: 'Chào anh Cường, có 3 căn hộ mới tại Quận 1 phù hợp.', status: NotificationStatus.DELIVERED, sentAt: oneWeekAgo },
|
|
{ id: 'seed-notif-002', userId: 'seed-buyer-001', channel: NotificationChannel.PUSH, templateKey: 'inquiry_replied', body: 'Agent An đã phản hồi câu hỏi về Vinhomes Central Park.', status: NotificationStatus.DELIVERED, sentAt: threeDaysAgo, readAt: threeDaysAgo },
|
|
{ id: 'seed-notif-003', userId: 'seed-agent-001', channel: NotificationChannel.EMAIL, templateKey: 'new_inquiry', subject: 'Câu hỏi mới về tin đăng', body: 'Lê Minh Cường đã gửi câu hỏi về Vinhomes Central Park 3PN.', status: NotificationStatus.DELIVERED, sentAt: threeDaysAgo },
|
|
{ id: 'seed-notif-004', userId: 'seed-agent-001', channel: NotificationChannel.SMS, templateKey: 'lead_new', body: 'GoodGo: Lead mới từ Nguyễn Thanh Long. Budget 8-10 tỷ.', status: NotificationStatus.SENT, sentAt: oneWeekAgo },
|
|
{ id: 'seed-notif-005', userId: 'seed-seller-001', channel: NotificationChannel.EMAIL, templateKey: 'listing_approved', subject: 'Tin đăng đã được duyệt', body: 'Tin "Căn hộ Vinhomes Central Park 3PN" đã duyệt.', status: NotificationStatus.DELIVERED, sentAt: oneWeekAgo },
|
|
{ id: 'seed-notif-006', userId: 'seed-buyer-002', channel: NotificationChannel.EMAIL, templateKey: 'payment_success', subject: 'Thanh toán thành công', body: 'Thanh toán 3.450.000.000 VND thành công.', status: NotificationStatus.DELIVERED, sentAt: oneWeekAgo },
|
|
{ id: 'seed-notif-007', userId: 'seed-agent-002', channel: NotificationChannel.PUSH, templateKey: 'subscription_renew', body: 'Gói Agent Pro sắp hết hạn. Gia hạn ngay!', status: NotificationStatus.DELIVERED, sentAt: threeDaysAgo },
|
|
{ id: 'seed-notif-008', userId: 'seed-admin-001', channel: NotificationChannel.EMAIL, templateKey: 'kyc_pending', subject: '2 yêu cầu KYC mới', body: 'Có 2 yêu cầu KYC đang chờ duyệt.', status: NotificationStatus.DELIVERED, sentAt: oneWeekAgo, readAt: oneWeekAgo },
|
|
{ id: 'seed-notif-009', userId: 'seed-buyer-001', channel: NotificationChannel.ZALO_OA, templateKey: 'viewing_reminder', body: 'Nhắc nhở: Lịch xem nhà Vinhomes Central Park 14:00 ngày mai.', status: NotificationStatus.PENDING },
|
|
{ id: 'seed-notif-010', userId: 'seed-seller-002', channel: NotificationChannel.EMAIL, templateKey: 'escrow_released', subject: 'Escrow đã giải ngân', body: 'Escrow 34.5 tỷ đã giải ngân. Phí nền tảng: 1.725 tỷ.', status: NotificationStatus.DELIVERED, sentAt: threeDaysAgo },
|
|
];
|
|
for (const n of logs) {
|
|
await prisma.notificationLog.upsert({ where: { id: n.id }, update: {}, create: n });
|
|
}
|
|
|
|
const prefs = [
|
|
{ id: 'seed-pref-001', userId: 'seed-buyer-001', channel: NotificationChannel.EMAIL, eventType: 'new_listing_match', enabled: true },
|
|
{ id: 'seed-pref-002', userId: 'seed-buyer-001', channel: NotificationChannel.PUSH, eventType: 'inquiry_replied', enabled: true },
|
|
{ id: 'seed-pref-003', userId: 'seed-buyer-001', channel: NotificationChannel.SMS, eventType: 'viewing_reminder', enabled: false },
|
|
{ id: 'seed-pref-004', userId: 'seed-agent-001', channel: NotificationChannel.EMAIL, eventType: 'new_inquiry', enabled: true },
|
|
{ id: 'seed-pref-005', userId: 'seed-agent-001', channel: NotificationChannel.SMS, eventType: 'lead_new', enabled: true },
|
|
{ id: 'seed-pref-006', userId: 'seed-seller-001', channel: NotificationChannel.EMAIL, eventType: 'listing_approved', enabled: true },
|
|
];
|
|
for (const p of prefs) {
|
|
await prisma.notificationPreference.upsert({ where: { userId_channel_eventType: { userId: p.userId, channel: p.channel, eventType: p.eventType } }, update: {}, create: p });
|
|
}
|
|
console.log(` ✓ ${logs.length} notifications + ${prefs.length} preferences seeded`);
|
|
}
|
|
|
|
async function seedAuditLogs() {
|
|
console.log('📝 Seeding admin audit logs...');
|
|
const logs = [
|
|
{ id: 'seed-audit-001', action: AdminAction.LISTING_APPROVED, actorId: 'seed-admin-001', targetId: 'seed-listing-001', targetType: AuditTargetType.LISTING, metadata: { reason: 'Đầy đủ thông tin, ảnh chất lượng tốt' }, ipAddress: '103.12.45.67' },
|
|
{ id: 'seed-audit-002', action: AdminAction.LISTING_APPROVED, actorId: 'seed-admin-001', targetId: 'seed-listing-003', targetType: AuditTargetType.LISTING, metadata: { reason: 'Đã xác minh sổ hồng' }, ipAddress: '103.12.45.67' },
|
|
{ id: 'seed-audit-003', action: AdminAction.KYC_APPROVED, actorId: 'seed-admin-001', targetId: 'seed-agent-001', targetType: AuditTargetType.USER, metadata: { documents: ['CCCD', 'Chứng chỉ BDS'] }, ipAddress: '103.12.45.67' },
|
|
{ id: 'seed-audit-004', action: AdminAction.KYC_APPROVED, actorId: 'seed-admin-001', targetId: 'seed-seller-001', targetType: AuditTargetType.USER, metadata: { documents: ['CCCD'] }, ipAddress: '103.12.45.67' },
|
|
{ id: 'seed-audit-005', action: AdminAction.LISTING_REJECTED, actorId: 'seed-admin-001', targetId: 'seed-listing-007', targetType: AuditTargetType.LISTING, metadata: { reason: 'Thiếu ảnh, pháp lý chưa rõ' }, ipAddress: '103.12.45.67' },
|
|
];
|
|
for (const l of logs) {
|
|
await prisma.adminAuditLog.upsert({ where: { id: l.id }, update: {}, create: l });
|
|
}
|
|
console.log(` ✓ ${logs.length} audit logs seeded`);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main seed
|
|
// =============================================================================
|
|
|
|
async function main() {
|
|
console.log('🌱 GoodGo Platform — Comprehensive Seed\n');
|
|
console.log('━'.repeat(60));
|
|
|
|
// Pre-compute password hash
|
|
console.log('🔑 Hashing default password...');
|
|
const passwordHash = bcrypt.hashSync(DEFAULT_PASSWORD, BCRYPT_ROUNDS);
|
|
console.log(` ✓ Password hash computed (bcrypt, ${BCRYPT_ROUNDS} rounds)\n`);
|
|
|
|
// Phase 1 — Plans
|
|
await seedPlans();
|
|
console.log('');
|
|
|
|
// Phase 2 — Users & Identity
|
|
await seedUsers(passwordHash);
|
|
await seedAgents();
|
|
await seedOAuthAccounts();
|
|
console.log('');
|
|
|
|
// Phase 2d — Project Developments (must come before Properties which reference them)
|
|
await seedProjectDevelopments();
|
|
console.log('');
|
|
|
|
// Phase 3 & 4 — Properties, Media, Listings
|
|
await seedProperties();
|
|
await seedListings();
|
|
console.log('');
|
|
|
|
// Phase 5 — Subscriptions
|
|
await seedSubscriptions();
|
|
console.log('');
|
|
|
|
// Phase 6 — Inquiries, Transactions, Leads
|
|
await seedInquiries();
|
|
await seedTransactions();
|
|
await seedLeads();
|
|
console.log('');
|
|
|
|
// Phase 7 — Orders, Escrow, Payments
|
|
await seedOrdersAndPayments();
|
|
console.log('');
|
|
|
|
// Phase 8 — Reviews, Valuations, Saved Searches
|
|
await seedReviews();
|
|
await seedValuations();
|
|
await seedSavedSearches();
|
|
console.log('');
|
|
|
|
// Phase 9 — Notifications & Audit
|
|
await seedNotifications();
|
|
await seedAuditLogs();
|
|
console.log('');
|
|
|
|
// Phase 10 — POIs & Neighborhood Scores
|
|
await seedPOIs(prisma);
|
|
console.log('');
|
|
|
|
// Phase 11 — Industrial Parks (KCN)
|
|
await seedIndustrialParks();
|
|
console.log('');
|
|
|
|
// Phase 11b — Industrial Listings
|
|
await seedIndustrialListings();
|
|
console.log('');
|
|
|
|
// Phase 12 — Macroeconomic Data & Infrastructure Projects
|
|
await seedMacroAndInfra(prisma);
|
|
console.log('');
|
|
|
|
// Phase 13 — Market Data
|
|
await importMarketData();
|
|
|
|
console.log('\n' + '━'.repeat(60));
|
|
console.log('✅ Comprehensive seed completed successfully!');
|
|
console.log('━'.repeat(60));
|
|
console.log('\n📋 Summary:');
|
|
console.log(' Users: 8 (1 admin, 3 agents, 2 buyers, 2 sellers)');
|
|
console.log(' Agents: 3 profiles');
|
|
console.log(' Projects: 10 developments (HCMC + Hanoi)');
|
|
console.log(' POIs: 60+ points of interest (HCMC)');
|
|
console.log(' Neighborhoods: 10 district scores');
|
|
console.log(' Properties: 10 + 20 media');
|
|
console.log(' Listings: 10');
|
|
console.log(' Plans: 4');
|
|
console.log(' Subscriptions: 6 + 8 usage records');
|
|
console.log(' Inquiries: 6');
|
|
console.log(' Transactions: 4');
|
|
console.log(' Leads: 6');
|
|
console.log(' Orders: 3');
|
|
console.log(' Escrows: 2');
|
|
console.log(' Payments: 6');
|
|
console.log(' Reviews: 8');
|
|
console.log(' Valuations: 5');
|
|
console.log(' Saved Searches: 4');
|
|
console.log(' Notifications: 10 + 6 prefs');
|
|
console.log(' Audit Logs: 5');
|
|
console.log(' Industrial: 20 parks + 12 listings');
|
|
console.log(' Macro Data: 144 data points (HCMC + Hanoi, quarterly)');
|
|
console.log(' Infra Projects: 15 (metro, highway, airport, bridge, port)');
|
|
console.log(' Market Index: ~240 records');
|
|
console.log('\n🔐 Admin Login:');
|
|
console.log(' Phone: 0876677771');
|
|
console.log(' Email: hongochai10@icloud.com');
|
|
console.log(' Password: Velik@2026');
|
|
console.log(' (All users share the same password)\n');
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error('❌ Seed failed:', e);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
});
|