Some checks failed
Security Scanning / Trivy Scan — API Image (push) Failing after 53s
Security Scanning / Trivy Scan — AI Services Image (push) Has been cancelled
Security Scanning / Trivy Filesystem Scan (push) Has been cancelled
Security Scanning / Security Gate (push) Has been cancelled
Security Scanning / Trivy Scan — Web Image (push) Has started running
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 10s
CI / AI Services (Python) — Smoke (push) Failing after 5s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 58s
Deploy / Build API Image (push) Failing after 18s
Deploy / Build Web Image (push) Failing after 7s
CI / E2E Tests (push) Has been skipped
Deploy / Build AI Services Image (push) Failing after 7s
E2E Tests / Playwright E2E (push) Failing after 16s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 4s
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
API bootstrap fixes (DI wiring):
- analytics.module: add forwardRef(() => AdminModule) to import
AI_CONFIG_PROVIDER for GetListingAiAdviceHandler + GetProjectAiAdviceHandler
- listings.module: add PaymentsModule to imports so PAYMENT_INITIATOR is
resolvable by FeatureListingHandler
- metrics.module: register 3 missing Prometheus providers that MetricsService
injects (READ_MODEL_PROJECTOR_LAG_SECONDS / REFRESH_DURATION /
RECONCILIATION_DRIFT_TOTAL) — caused boot failure previously
- get-listing-ai-advice.handler: switch LISTING_REPOSITORY import from barrel
@modules/listings to direct internal path to break circular reference that
made the symbol evaluate as undefined at decorator time
- shared.module: comment out broken EVENT_BUS / OutboxService / OutboxRelay
providers (depend on @goodgo/contracts-events workspace pkg not yet wired)
CSRF middleware:
- Rewrite exclude logic as inline path-check inside the middleware itself.
Nest 11 + path-to-regexp v8 changed how MiddlewareConsumer.exclude() matches
against forRoutes('*') — the previous string patterns silently stopped
matching, causing every POST to /auth/login to return 403 CSRF Forbidden.
Inlined exempt list strips the /api/v1 prefix and checks against a Set.
Admin revenue stats:
- admin-stats.queries: use Prisma.sql template fragments for DATE_TRUNC unit
('day'|'month'). Passing the unit as a bind parameter caused Postgres error
42803 (column must appear in GROUP BY) because the planner treats $1 as an
opaque scalar and cannot prove SELECT and GROUP BY expressions are equal.
Admin audit-log page:
- SeverityPill: add ?? 'info' fallback — backend AuditLogEntry does not
include a `severity` field, so SEVERITY_CONFIG[undefined] was undefined
and .dir threw TypeError, crashing the whole audit-log page.
DB seed fixes:
- seed.ts: replace Vietnamese enum literals ('Sổ hồng', 'Sổ đỏ') with
correct enum keys ('SO_HONG', 'SO_DO') for the LegalStatus column
- seed-industrial-parks.ts: gate the standalone main() behind
require.main === module so importing the file from seed.ts doesn't
immediately close the pg.Pool used by the orchestrator
- scripts/seed-industrial-listings.ts: restore from tmp/ stash; was missing
from scripts/ causing seed.ts import to fail at startup
- migration 20260429010000_add_property_certificate_verified: Property table
was missing the certificateVerified column required by seed + Prisma schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
813 lines
60 KiB
TypeScript
813 lines
60 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 { 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
|
|
// =============================================================================
|
|
|
|
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 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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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ủ Đứ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();
|
|
});
|