/* eslint-disable no-console */ const path = require('path'); const fs = require('fs'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); if (fs.existsSync(path.join(__dirname, '..', 'creds', '.env'))) { require('dotenv').config({ path: path.join(__dirname, '..', 'creds', '.env') }); } const bcrypt = require('bcryptjs'); const { PrismaClient, Role, UserStatus, ListingStatus } = require('@prisma/client'); const { PrismaPg } = require('@prisma/adapter-pg'); const { Pool } = require('pg'); if (!process.env.DATABASE_URL) { console.error('DATABASE_URL is not set; cannot seed.'); process.exit(1); } const prisma = new PrismaClient({ adapter: new PrismaPg(new Pool({ connectionString: process.env.DATABASE_URL })), }); const SAMPLE_SLUG = 'saimaa-lakeside-cabin'; const DEFAULT_LOCALE = 'en'; const SAMPLE_EMAIL = 'host@lomavuokraus.fi'; const SAMPLE_IMAGE_DIR = path.join(__dirname, '..', 'sampleimages'); function detectMimeType(fileName) { const ext = path.extname(fileName).toLowerCase(); if (ext === '.png') return 'image/png'; if (ext === '.webp') return 'image/webp'; if (ext === '.gif') return 'image/gif'; return 'image/jpeg'; } function loadSampleImage(fileName) { if (!fileName) return null; const filePath = path.join(SAMPLE_IMAGE_DIR, fileName); if (!fs.existsSync(filePath)) { console.warn(`Sample image missing: ${filePath}`); return null; } const data = fs.readFileSync(filePath); return { data, size: data.length, mimeType: detectMimeType(fileName), }; } async function main() { const adminEmail = process.env.ADMIN_EMAIL; const adminPassword = process.env.ADMIN_INITIAL_PASSWORD; function buildImageCreates(item) { const results = []; const coverFile = loadSampleImage(item.cover.file); if (coverFile || item.cover.url) { results.push({ data: coverFile?.data, mimeType: coverFile?.mimeType || (item.cover.url ? null : undefined), size: coverFile?.size ?? null, url: coverFile ? null : item.cover.url ?? null, altText: item.cover.altText ?? null, order: 1, isCover: true, }); } item.images.forEach((img) => { const file = loadSampleImage(img.file); const hasSource = file || img.url; if (!hasSource) return; results.push({ data: file?.data, mimeType: file?.mimeType || (img.url ? null : undefined), size: file?.size ?? null, url: file ? null : img.url ?? null, altText: img.altText ?? null, order: results.length + 1, isCover: false, }); }); return results; } if (!adminEmail || !adminPassword) { console.warn('ADMIN_EMAIL or ADMIN_INITIAL_PASSWORD missing; admin user will not be seeded.'); } let adminUser = null; if (adminEmail && adminPassword) { const adminHash = await bcrypt.hash(adminPassword, 12); adminUser = await prisma.user.upsert({ where: { email: adminEmail }, update: { passwordHash: adminHash, role: Role.ADMIN, status: UserStatus.ACTIVE, emailVerifiedAt: new Date(), approvedAt: new Date(), }, create: { email: adminEmail, passwordHash: adminHash, role: Role.ADMIN, status: UserStatus.ACTIVE, emailVerifiedAt: new Date(), approvedAt: new Date(), }, }); } const sampleHostHash = await bcrypt.hash('changeme-sample', 12); const owner = await prisma.user.upsert({ where: { email: SAMPLE_EMAIL }, update: { name: 'Sample Host', phone: '+358401234567', role: 'USER', passwordHash: sampleHostHash, status: UserStatus.ACTIVE, emailVerifiedAt: new Date(), approvedAt: new Date(), }, create: { email: SAMPLE_EMAIL, name: 'Sample Host', phone: '+358401234567', role: 'USER', passwordHash: sampleHostHash, status: UserStatus.ACTIVE, emailVerifiedAt: new Date(), approvedAt: new Date(), }, }); const listings = [ { slug: SAMPLE_SLUG, isSample: true, city: 'Punkaharju', region: 'South Karelia', country: 'Finland', streetAddress: 'Saimaan rantatie 12', addressNote: 'Lakeside trail, 5 min from main road', latitude: 61.756, longitude: 29.328, maxGuests: 6, bedrooms: 3, beds: 4, bathrooms: 1, hasSauna: true, hasFireplace: true, hasWifi: true, hasAirConditioning: false, petsAllowed: false, byTheLake: true, evCharging: 'FREE', priceHintPerNightEuros: 145, cover: { file: 'saimaa-lakeside-cabin-cover.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Lakeside cabin with sauna', }, images: [ { file: 'saimaa-lakeside-cabin-sauna.jpg', url: 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=1600&q=80', altText: 'Wood-fired sauna by the lake' }, { file: 'saimaa-lakeside-cabin-lounge.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Living area with fireplace' }, ], titleEn: 'Saimaa lakeside cabin with sauna', teaserEn: 'Sauna, lake view, private dock, and cozy fireplace.', descEn: 'Classic timber cabin right on Lake Saimaa. Wood-fired sauna, private dock, and a short forest walk to the nearest village. Perfect for slow weekends and midsummer gatherings.', titleFi: 'Saimaan rantamökki saunalla', teaserFi: 'Puusauna, järvinäköala, oma laituri ja takka.', descFi: 'Perinteinen hirsimökki Saimaan rannalla. Puusauna, oma laituri ja lyhyt metsäreitti kylään. Sopii täydellisesti viikonloppuihin ja juhannukseen.', }, { slug: 'helsinki-design-loft', isSample: true, city: 'Helsinki', region: 'Uusimaa', country: 'Finland', streetAddress: 'Katajanokanranta 4', addressNote: 'Buzz 12B, elevator to 5th floor', latitude: 60.1675, longitude: 24.9529, maxGuests: 4, bedrooms: 1, beds: 2, bathrooms: 1, hasSauna: false, hasFireplace: false, hasWifi: true, hasAirConditioning: true, petsAllowed: false, byTheLake: false, evCharging: 'PAID', priceHintPerNightEuros: 165, cover: { file: 'helsinki-design-loft-cover.jpg', url: 'https://images.unsplash.com/photo-1505693415763-3bd1620f58c3?auto=format&fit=crop&w=1600&q=80', altText: 'Modern loft living room', }, images: [ { file: 'helsinki-design-loft-balcony.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Balcony view' }, { file: 'helsinki-design-loft-bedroom.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Cozy bedroom' }, ], titleEn: 'Helsinki design loft with AC', teaserEn: 'Top-floor loft, AC, fast Wi-Fi, tram at the door.', descEn: 'Bright design loft near the harbor. Air conditioning, fiber Wi-Fi, and views over Katajanokka. Perfect city break base.', titleFi: 'Helsingin design-lofti ilmastoinnilla', teaserFi: 'Ylimmän kerroksen loft, ilmastointi ja nopea netti.', descFi: 'Valoisa loft Katajanokalla. Ilmastointi, kuitunetti ja näkymä merelle. Täydellinen kaupunkiloma.', }, { slug: 'turku-riverside-apartment', isSample: true, city: 'Turku', region: 'Varsinais-Suomi', country: 'Finland', streetAddress: 'Läntinen Rantakatu 10', addressNote: 'Self check-in lockbox', latitude: 60.4518, longitude: 22.2666, maxGuests: 3, bedrooms: 1, beds: 2, bathrooms: 1, hasSauna: false, hasFireplace: false, hasWifi: true, hasAirConditioning: false, petsAllowed: true, byTheLake: false, evCharging: 'NONE', priceHintPerNightEuros: 110, cover: { file: 'turku-riverside-apartment-cover.jpg', url: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=1600&q=80', altText: 'Apartment living room', }, images: [ { file: 'turku-riverside-apartment-kitchen.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Kitchen area' }, ], titleEn: 'Riverside apartment in Turku', teaserEn: 'By the Aura river, pet-friendly, cozy base.', descEn: 'Compact one-bedroom next to the Aura river. Cafés outside, pet-friendly, fiber internet for workations.', titleFi: 'Aurajoen varrella, lemmikkiystävällinen', teaserFi: 'Aurajoen kupeessa, lemmikit sallittu.', descFi: 'Kompakti yksiö Aurajoen varrella. Kahvilat vieressä, lemmikit sallittu, kuitunetti etätöihin.', }, { slug: 'rovaniemi-aurora-cabin', isSample: true, city: 'Rovaniemi', region: 'Lapland', country: 'Finland', streetAddress: 'Ounasjoenkuja 8', addressNote: 'Snow tires required in winter', latitude: 66.5039, longitude: 25.7294, maxGuests: 5, bedrooms: 2, beds: 4, bathrooms: 1, hasSauna: true, hasFireplace: true, hasWifi: true, hasAirConditioning: false, petsAllowed: false, byTheLake: true, evCharging: 'FREE', priceHintPerNightEuros: 189, cover: { file: 'rovaniemi-aurora-cabin-cover.jpg', url: 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=1600&q=80', altText: 'Aurora cabin by the river', }, images: [ { file: 'rovaniemi-aurora-cabin-lounge.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Fireplace lounge' }, ], titleEn: 'Aurora riverside cabin', teaserEn: 'Sauna, fireplace, river views, EV charging.', descEn: 'Timber cabin on the Ounasjoki riverside. Wood sauna, fireplace, glass lounge for auroras, free EV charging.', titleFi: 'Revontulikämppä joen rannalla', teaserFi: 'Sauna, takka, jokinäkymä ja ilmainen lataus.', descFi: 'Hirsimökki Ounasjoen rannalla. Puusauna, takka ja lasikuisti revontulien katseluun, ilmainen sähköauton lataus.', }, { slug: 'tampere-sauna-studio', isSample: true, city: 'Tampere', region: 'Pirkanmaa', country: 'Finland', streetAddress: 'Hämeenkatu 25', addressNote: 'Key pickup from lobby', latitude: 61.4981, longitude: 23.7608, maxGuests: 2, bedrooms: 0, beds: 1, bathrooms: 1, hasSauna: true, hasFireplace: false, hasWifi: true, hasAirConditioning: true, petsAllowed: false, byTheLake: false, evCharging: 'NONE', priceHintPerNightEuros: 95, cover: { file: 'tampere-sauna-studio-cover.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Studio interior', }, images: [{ file: 'tampere-sauna-studio-sauna.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Private sauna' }], titleEn: 'Tampere studio with private sauna', teaserEn: 'City center, private sauna, AC and fiber.', descEn: 'Compact studio on Hämeenkatu with private electric sauna, air conditioning, and fiber internet. Steps from tram.', titleFi: 'Tampereen keskustastudio saunalla', teaserFi: 'Yksityinen sauna, ilmastointi, kuitu.', descFi: 'Kompakti studio Hämeenkadulla. Oma sähkösauna, ilmastointi ja kuitunetti. Ratikka vieressä.', }, { slug: 'vaasa-seaside-villa', isSample: true, city: 'Vaasa', region: 'Ostrobothnia', country: 'Finland', streetAddress: 'Rantakatu 3', addressNote: 'Parking for 3 cars', latitude: 63.096, longitude: 21.6158, maxGuests: 8, bedrooms: 4, beds: 6, bathrooms: 2, hasSauna: true, hasFireplace: true, hasWifi: true, hasAirConditioning: false, petsAllowed: true, byTheLake: true, evCharging: 'PAID', priceHintPerNightEuros: 245, cover: { file: 'vaasa-seaside-villa-cover.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Seaside villa deck', }, images: [{ file: 'vaasa-seaside-villa-lounge.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Seaside villa lounge' }], titleEn: 'Seaside villa in Vaasa', teaserEn: 'Deck, sauna, pet-friendly, paid EV charging.', descEn: 'Spacious villa on the coast with large deck, wood sauna, fireplace lounge. Pets welcome; paid EV charger on site.', titleFi: 'Rantahuvila Vaasassa', teaserFi: 'Terassi, sauna, lemmikit ok, maksullinen lataus.', descFi: 'Tilava huvila meren rannalla, iso terassi, puusauna ja takkahuone. Lemmikit sallittu; maksullinen latauspiste.', }, { slug: 'kuopio-lakeside-apartment', isSample: true, city: 'Kuopio', region: 'Northern Savonia', country: 'Finland', streetAddress: 'Satamakatu 7', addressNote: 'Underground parking', latitude: 62.8924, longitude: 27.6783, maxGuests: 4, bedrooms: 2, beds: 3, bathrooms: 1, hasSauna: true, hasFireplace: false, hasWifi: true, hasAirConditioning: false, petsAllowed: false, byTheLake: true, evCharging: 'FREE', priceHintPerNightEuros: 129, cover: { file: 'kuopio-lakeside-apartment-cover.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Lake view balcony', }, images: [{ file: 'kuopio-lakeside-apartment-sauna.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Apartment sauna' }], titleEn: 'Kuopio lakeside apartment with sauna', teaserEn: 'Balcony to Kallavesi, sauna, free EV charging.', descEn: 'Two-bedroom apartment overlooking Lake Kallavesi. Glass balcony, electric sauna, underground parking with free EV charging.', titleFi: 'Kuopion järvinäkymä ja sauna', teaserFi: 'Parveke Kallavedelle, sauna, ilmainen lataus.', descFi: 'Kaksio Kallaveden rannalla. Lasitettu parveke, sähkösauna, hallipaikka ja ilmainen sähköauton lataus.', }, { slug: 'porvoo-river-loft', isSample: true, city: 'Porvoo', region: 'Uusimaa', country: 'Finland', streetAddress: 'Mannerheiminkatu 12', addressNote: 'Historic building, stairs only', latitude: 60.3943, longitude: 25.6659, maxGuests: 2, bedrooms: 1, beds: 1, bathrooms: 1, hasSauna: false, hasFireplace: true, hasWifi: true, hasAirConditioning: false, petsAllowed: false, byTheLake: false, evCharging: 'NONE', priceHintPerNightEuros: 99, cover: { file: 'porvoo-river-loft-cover.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Loft interior', }, images: [{ file: 'porvoo-river-loft-fireplace.jpg', url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80', altText: 'Fireplace corner' }], titleEn: 'Porvoo old town river loft', teaserEn: 'Historic charm, fireplace, steps from river.', descEn: 'Cozy loft in Porvoo old town. Brick walls, fireplace, and views toward the riverside warehouses.', titleFi: 'Porvoon jokilofti', teaserFi: 'Takka ja vanhan kaupungin tunnelma.', descFi: 'Kotoisa loft Porvoon vanhassa kaupungissa. Tiiliseinät, takka ja näkymä jokirantaan.', }, { slug: 'oulu-tech-apartment', isSample: true, city: 'Oulu', region: 'Northern Ostrobothnia', country: 'Finland', streetAddress: 'Technopolis 2', addressNote: 'Smart lock entry', latitude: 65.0121, longitude: 25.4651, maxGuests: 2, bedrooms: 1, beds: 1, bathrooms: 1, hasSauna: false, hasFireplace: false, hasWifi: true, hasAirConditioning: true, petsAllowed: false, byTheLake: false, evCharging: 'FREE', priceHintPerNightEuros: 105, cover: { file: 'oulu-tech-apartment-cover.jpg', url: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=1600&q=80', altText: 'Modern apartment', }, images: [{ file: 'oulu-tech-apartment-desk.jpg', url: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=1600&q=80', altText: 'Work desk in apartment' }], titleEn: 'Smart apartment in Oulu', teaserEn: 'AC, smart lock, free EV charging in garage.', descEn: 'Modern one-bedroom near Technopolis. Air conditioning, smart lock, desk for work, garage with free EV chargers.', titleFi: 'Moderni Oulun yksiö', teaserFi: 'Ilmastointi, älylukko, ilmainen lataus.', descFi: 'Moderni yksiö Technopoliksen lähellä. Ilmastointi, älylukko, työpiste ja ilmaiset latauspaikat hallissa.', }, { slug: 'mariehamn-harbor-flat', isSample: true, city: 'Mariehamn', region: 'Åland', country: 'Finland', streetAddress: 'Hamngatan 5', addressNote: 'Ferry terminal 10 min walk', latitude: 60.0973, longitude: 19.9348, maxGuests: 3, bedrooms: 1, beds: 2, bathrooms: 1, hasSauna: false, hasFireplace: false, hasWifi: true, hasAirConditioning: false, petsAllowed: false, byTheLake: true, evCharging: 'PAID', priceHintPerNightEuros: 115, cover: { file: 'mariehamn-harbor-flat-cover.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Harbor view', }, images: [{ file: 'mariehamn-harbor-flat-living.jpg', url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80', altText: 'Harbor-facing living room' }], titleEn: 'Harbor flat in Mariehamn', teaserEn: 'Walk to ferries, harbor views, paid EV nearby.', descEn: 'Bright flat near the harbor. Walk to ferries and restaurants, harbor-facing balcony, paid EV charging at the public lot.', titleFi: 'Satamahuoneisto Maarianhaminassa', teaserFi: 'Satamanäkymä, kävely lautoille.', descFi: 'Valoisa huoneisto sataman tuntumassa. Parveke satamaan, ravintolat lähellä, maksullinen lataus viereisellä parkkipaikalla.', }, ]; for (const item of listings) { const existing = await prisma.listingTranslation.findFirst({ where: { slug: item.slug }, select: { listingId: true } }); const imageCreates = buildImageCreates(item); if (!existing) { const created = await prisma.listing.create({ data: { ownerId: owner.id, status: ListingStatus.PUBLISHED, approvedAt: new Date(), approvedById: adminUser ? adminUser.id : owner.id, isSample: item.isSample ?? false, country: item.country, region: item.region, city: item.city, streetAddress: item.streetAddress, addressNote: item.addressNote, latitude: item.latitude, longitude: item.longitude, maxGuests: item.maxGuests, bedrooms: item.bedrooms, beds: item.beds, bathrooms: item.bathrooms, hasSauna: item.hasSauna, hasFireplace: item.hasFireplace, hasWifi: item.hasWifi, hasAirConditioning: item.hasAirConditioning, petsAllowed: item.petsAllowed, byTheLake: item.byTheLake, evCharging: item.evCharging, priceHintPerNightEuros: item.priceHintPerNightEuros, contactName: 'Sample Host', contactEmail: SAMPLE_EMAIL, contactPhone: owner.phone, published: true, translations: { createMany: { data: [ { locale: 'en', slug: item.slug, title: item.titleEn, teaser: item.teaserEn, description: item.descEn }, { locale: 'fi', slug: item.slug, title: item.titleFi, teaser: item.teaserFi, description: item.descFi }, ], }, }, images: imageCreates.length ? { create: imageCreates } : undefined, }, }); console.log('Seeded listing:', created.id, item.slug); continue; } const listingId = existing.listingId; await prisma.listing.update({ where: { id: listingId }, data: { isSample: item.isSample ?? false, country: item.country, region: item.region, city: item.city, streetAddress: item.streetAddress, addressNote: item.addressNote, latitude: item.latitude, longitude: item.longitude, maxGuests: item.maxGuests, bedrooms: item.bedrooms, beds: item.beds, bathrooms: item.bathrooms, hasSauna: item.hasSauna, hasFireplace: item.hasFireplace, hasWifi: item.hasWifi, hasAirConditioning: item.hasAirConditioning, petsAllowed: item.petsAllowed, byTheLake: item.byTheLake, evCharging: item.evCharging, priceHintPerNightEuros: item.priceHintPerNightEuros, contactName: 'Sample Host', contactEmail: SAMPLE_EMAIL, contactPhone: owner.phone, published: true, status: ListingStatus.PUBLISHED, approvedAt: new Date(), }, }); await prisma.listingTranslation.upsert({ where: { slug_locale: { slug: item.slug, locale: 'en' } }, create: { listingId, locale: 'en', slug: item.slug, title: item.titleEn, teaser: item.teaserEn, description: item.descEn }, update: { title: item.titleEn, teaser: item.teaserEn, description: item.descEn }, }); await prisma.listingTranslation.upsert({ where: { slug_locale: { slug: item.slug, locale: 'fi' } }, create: { listingId, locale: 'fi', slug: item.slug, title: item.titleFi, teaser: item.teaserFi, description: item.descFi }, update: { title: item.titleFi, teaser: item.teaserFi, description: item.descFi }, }); await prisma.listingImage.deleteMany({ where: { listingId } }); if (imageCreates.length) { await prisma.listingImage.createMany({ data: imageCreates.map((img) => ({ ...img, listingId, })), }); } console.log('Updated listing:', item.slug); } console.log('Seed completed for sample listings.'); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });