Files
goodgo-platform/prisma/seed.ts
Ho Ngoc Hai a9fa214544 feat: comprehensive seed, Lucide icons, grouped dashboard nav, API fixes
- Rewrite prisma/seed.ts to populate all 27 models with realistic
  Vietnamese real estate data (8 users with login, 10 properties,
  10 listings, orders, payments, reviews, notifications, etc.)
- Replace all emoji icons with Lucide React SVG icons across frontend
  for consistent rendering, sizing, and accessibility
- Redesign dashboard nav: grouped sidebar with section headers,
  primary/secondary split on desktop, icon-only secondary items
- Replace language switcher flag emoji with Globe icon
- Replace SVG theme toggle with Lucide Moon/Sun icons
- Fix API startup: graceful fallback for Sentry profiling, Google OAuth,
  and Zalo OAuth when credentials are not configured
- Relax rate limiting in development mode (10k req/min)
- Fix listings API to include media[] array in search response
- Add optional chaining for property.media across frontend components
- Update OAuth strategy tests to match graceful fallback behavior

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-13 11:13:04 +07:00

559 lines
42 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 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();
});