/** * 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 path from 'node:path'; 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, } from '@prisma/client'; import pg from 'pg'; // bcrypt is installed in apps/api — resolve from there import bcrypt from 'bcrypt'; import { seedPlans } from '../scripts/seed-plans'; import { importMarketData } from '../scripts/import-market-data'; 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 // ============================================================================= 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) { await prisma.user.upsert({ where: { id: u.id }, update: { passwordHash, email: u.email, fullName: u.fullName, role: u.role, kycStatus: u.kycStatus }, create: { id: u.id, phone: u.phone, email: u.email, passwordHash, 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 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: 'Sổ hồng', 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ủ Đức', city: 'Hồ Chí Minh', lat: 10.7696, lng: 106.7511, areaM2: 76, usableAreaM2: 68, bedrooms: 2, bathrooms: 2, floor: 15, totalFloors: 28, direction: Direction.NORTH, yearBuilt: 2020, legalStatus: 'Sổ hồng', 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ủ Đức', city: 'Hồ Chí Minh', lat: 10.8033, lng: 106.7391, areaM2: 200, usableAreaM2: 350, bedrooms: 4, bathrooms: 5, floors: 4, direction: Direction.SOUTH, yearBuilt: 2015, legalStatus: 'Sổ hồng', 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: 'Sổ đỏ', 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: 'Sổ hồng', 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ủ Đứ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: 'Sổ hồng', 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ủ Đứ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: 'Sổ hồng', 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ủ Đứ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: 'Sổ hồng', 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: 'Sổ hồng', 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: 'Sổ hồng', 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' }, }); } } console.log(` ✓ ${properties.length} properties + ${properties.length * 2} media seeded`); } // ============================================================================= // 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ủ Đứ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 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 — 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(' 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(' 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(); });