import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: resolve(__dirname, '../../../../.env') }); import { db, sql } from './index.js'; import { platforms, performanceSnapshots, dailyPrices, priceChanges, experiments, reservations, scrapeJobs, } from './schema.js'; // ── Helpers ───────────────────────────────────────────────────────────────── function rand(min: number, max: number): number { return Math.random() * (max - min) + min; } function randInt(min: number, max: number): number { return Math.floor(rand(min, max + 1)); } function roundTo(n: number, decimals: number): string { return n.toFixed(decimals); } function daysAgo(n: number): Date { const d = new Date(); d.setDate(d.getDate() - n); d.setHours(0, 0, 0, 0); return d; } function formatDate(d: Date): string { return d.toISOString().split('T')[0]; } function isWeekend(d: Date): boolean { const day = d.getDay(); return day === 0 || day === 5 || day === 6; // Fri, Sat, Sun } const GUEST_NAMES = [ 'Sarah Johnson', 'Michael Chen', 'Emily Rodriguez', 'James Wilson', 'Amanda Foster', 'David Kim', 'Jessica Taylor', 'Robert Martinez', 'Lisa Anderson', 'Chris Thompson', 'Rachel Lee', 'Daniel Brown', 'Maria Garcia', 'Kevin Wright', 'Stephanie Davis', 'Andrew Moore', 'Nicole Clark', 'Brandon Hall', ]; // ── Seed Logic ────────────────────────────────────────────────────────────── async function seed() { console.log('Clearing existing data...'); // Delete in FK-safe order await db.delete(priceChanges); await db.delete(scrapeJobs); await db.delete(reservations); await db.delete(dailyPrices); await db.delete(performanceSnapshots); await db.delete(experiments); await db.delete(platforms); console.log('Inserting platforms...'); await db.insert(platforms).values([ { id: 'airbnb', displayName: 'Airbnb' }, { id: 'vrbo', displayName: 'VRBO' }, { id: 'mock', displayName: 'Mock (Dev)' }, ]); // ── Experiments ─────────────────────────────────────────────────────────── console.log('Inserting experiments...'); const experimentRows = [ { name: 'Weekend Premium Pricing', hypothesis: 'Increasing weekend rates by 15% will maintain occupancy while boosting revenue.', startDate: formatDate(daysAgo(60)), endDate: formatDate(daysAgo(30)), status: 'completed', conclusion: 'Weekend premium of 15% maintained 92% of baseline bookings while increasing weekend revenue by 11%.', }, { name: 'Minimum Stay Reduction', hypothesis: 'Reducing minimum stay from 3 nights to 2 nights on weekdays will fill gap nights and increase occupancy.', startDate: formatDate(daysAgo(30)), endDate: formatDate(daysAgo(7)), status: 'completed', conclusion: 'Occupancy rose 8% during the test period. Gap nights filled at an acceptable rate. Recommend keeping 2-night min on weekdays.', }, { name: 'Dynamic Last-Minute Discount', hypothesis: 'Offering a 10% discount for bookings within 3 days of check-in will reduce vacancy on otherwise-empty nights.', startDate: formatDate(daysAgo(14)), status: 'active', }, ]; const insertedExperiments = await db .insert(experiments) .values(experimentRows) .returning({ id: experiments.id }); // ── Performance Snapshots ───────────────────────────────────────────────── console.log('Inserting performance snapshots...'); const snapshotRows: (typeof performanceSnapshots.$inferInsert)[] = []; for (const platformId of ['airbnb', 'vrbo']) { const platformMultiplier = platformId === 'airbnb' ? 1.0 : 0.6; for (let daysBack = 89; daysBack >= 0; daysBack--) { const d = daysAgo(daysBack); // Slight upward trend: later days get a small boost const trendFactor = 1 + (90 - daysBack) * 0.001; const viewsSearch = Math.round( randInt(500, 2000) * platformMultiplier * trendFactor, ); const viewsListing = Math.round( randInt(100, 500) * platformMultiplier * trendFactor, ); const occupancy = Math.min( rand(55, 85) * trendFactor, 98, ); const adr = rand(150, 250) * trendFactor; const bookingsCount = Math.round( randInt(2, 8) * platformMultiplier * (occupancy / 70), ); const conversionRate = (bookingsCount / viewsListing) * 100; const revenue = adr * occupancy * 0.3; // rough approximation snapshotRows.push({ platformId, capturedAt: d.toISOString(), periodLabel: formatDate(d), viewsSearch, viewsListing, conversionRate: roundTo(conversionRate, 2), bookingsCount, occupancyRate: roundTo(occupancy, 1), avgDailyRate: roundTo(adr, 2), revenueTotal: roundTo(revenue, 2), rawJson: { source: 'seed', generated: true }, }); } } // Insert in batches for (let i = 0; i < snapshotRows.length; i += 50) { await db .insert(performanceSnapshots) .values(snapshotRows.slice(i, i + 50)); } // ── Daily Prices ────────────────────────────────────────────────────────── console.log('Inserting daily prices...'); const priceRows: (typeof dailyPrices.$inferInsert)[] = []; for (const platformId of ['airbnb', 'vrbo']) { for (let daysBack = 89; daysBack >= -30; daysBack--) { // Include 30 days into the future const d = daysAgo(daysBack); const trendFactor = 1 + (90 - daysBack) * 0.0005; let basePrice = rand(150, 250) * trendFactor; if (isWeekend(d)) { basePrice *= rand(1.15, 1.35); // Weekend premium } priceRows.push({ platformId, date: formatDate(d), price: roundTo(basePrice, 2), isAvailable: Math.random() > 0.15, // ~85% availability minStayNights: isWeekend(d) ? 2 : 1, syncedAt: daysAgo(Math.max(daysBack, 0)).toISOString(), }); } } for (let i = 0; i < priceRows.length; i += 50) { await db.insert(dailyPrices).values(priceRows.slice(i, i + 50)); } // ── Reservations ────────────────────────────────────────────────────────── console.log('Inserting reservations...'); const reservationRows: (typeof reservations.$inferInsert)[] = []; let reservationCounter = 1000; for (let i = 0; i < 15; i++) { const platformId = i % 3 === 0 ? 'vrbo' : 'airbnb'; const checkInDaysAgo = randInt(5, 85); const nights = randInt(2, 7); const checkIn = daysAgo(checkInDaysAgo); const checkOut = daysAgo(checkInDaysAgo - nights); const nightlyRate = rand(160, 280); const cleaningFee = randInt(75, 150); const platformFee = nightlyRate * nights * rand(0.03, 0.05); const totalPayout = nightlyRate * nights + cleaningFee - platformFee; const bookedDaysBeforeCheckIn = randInt(3, 30); const bookedAt = daysAgo(checkInDaysAgo + bookedDaysBeforeCheckIn); reservationCounter++; const isPast = checkInDaysAgo - nights > 0; reservationRows.push({ platformId, platformReservationId: `${platformId.toUpperCase()}-${reservationCounter}`, guestName: GUEST_NAMES[i % GUEST_NAMES.length], checkIn: formatDate(checkIn), checkOut: formatDate(checkOut), nights, guestsCount: randInt(1, 6), nightlyRate: roundTo(nightlyRate, 2), cleaningFee: cleaningFee.toString(), platformFee: roundTo(platformFee, 2), totalPayout: roundTo(totalPayout, 2), status: isPast ? 'completed' : 'confirmed', bookedAt: bookedAt.toISOString(), rawJson: { source: 'seed', generated: true }, }); } await db.insert(reservations).values(reservationRows); // ── Price Changes ───────────────────────────────────────────────────────── console.log('Inserting price changes...'); const priceChangeRows: (typeof priceChanges.$inferInsert)[] = []; // Link some price changes to the first experiment (weekend premium) for (let i = 0; i < 8; i++) { const d = daysAgo(randInt(30, 60)); const before = rand(180, 230); const after = before * 1.15; priceChangeRows.push({ platformId: i % 2 === 0 ? 'airbnb' : 'vrbo', date: formatDate(d), priceBefore: roundTo(before, 2), priceAfter: roundTo(after, 2), changedBy: 'experiment', note: 'Weekend premium pricing experiment', experimentId: insertedExperiments[0].id, }); } // A few manual changes for (let i = 0; i < 4; i++) { const d = daysAgo(randInt(5, 25)); const before = rand(170, 250); const after = before * rand(0.9, 1.1); priceChangeRows.push({ platformId: 'airbnb', date: formatDate(d), priceBefore: roundTo(before, 2), priceAfter: roundTo(after, 2), changedBy: 'manual', note: 'Manual price adjustment', }); } await db.insert(priceChanges).values(priceChangeRows); // ── Scrape Jobs ─────────────────────────────────────────────────────────── console.log('Inserting scrape jobs...'); const scrapeRows: (typeof scrapeJobs.$inferInsert)[] = []; for (const platformId of ['airbnb', 'vrbo']) { for (let i = 0; i < 5; i++) { const startedDaysAgo = randInt(0, 30); const started = daysAgo(startedDaysAgo); const completed = new Date( started.getTime() + randInt(5, 120) * 1000, ); scrapeRows.push({ platformId, jobType: i % 2 === 0 ? 'performance' : 'calendar', triggeredBy: i === 0 ? 'manual' : 'scheduled', status: 'completed', startedAt: started.toISOString(), completedAt: completed.toISOString(), rowsCollected: randInt(10, 200), }); } } await db.insert(scrapeJobs).values(scrapeRows); console.log('Seed complete.'); } seed() .then(() => { process.exit(0); }) .catch((err) => { console.error('Seed failed:', err); process.exit(1); });