import { db } from './db'; import bcrypt from 'bcryptjs'; export async function initSchema() { await db.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')), comments_disabled BOOLEAN DEFAULT FALSE, first_name VARCHAR(100), last_name VARCHAR(100), created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS images ( id SERIAL PRIMARY KEY, filename VARCHAR(255) NOT NULL, data TEXT NOT NULL, thumbnail TEXT, medium TEXT, category VARCHAR(100) DEFAULT 'other', room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL, location_target VARCHAR(100) DEFAULT 'gallery', uploaded_at TIMESTAMP DEFAULT NOW() ); `); // Add columns if they don't exist (for existing databases) await db.query(`ALTER TABLE images ADD COLUMN IF NOT EXISTS medium TEXT`); await db.query(`ALTER TABLE images ADD COLUMN IF NOT EXISTS thumbnail TEXT`); await db.query(` CREATE TABLE IF NOT EXISTS reservations ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, date DATE NOT NULL, time TIME NOT NULL, guests INTEGER NOT NULL, table_name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS coupons ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, code VARCHAR(50) NOT NULL, discount VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS offers ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS benefits ( id SERIAL PRIMARY KEY, text VARCHAR(500) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS config ( key VARCHAR(100) PRIMARY KEY, value TEXT NOT NULL ); `); await db.query(` CREATE TABLE IF NOT EXISTS rooms ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, price NUMERIC(10,2) NOT NULL, photos TEXT[] DEFAULT '{}', featured_photo VARCHAR(500), active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS menu_items ( id SERIAL PRIMARY KEY, category VARCHAR(100) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, price NUMERIC(10,2) NOT NULL, is_daily_special BOOLEAN DEFAULT FALSE, active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, photos TEXT[] DEFAULT '{}', featured_photo VARCHAR(500), created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS places ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, photos TEXT[] DEFAULT '{}', featured_photo VARCHAR(500), distance_km NUMERIC(8,2), visit_time VARCHAR(100), suggestion TEXT, active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS reviews ( id SERIAL PRIMARY KEY, author_name VARCHAR(255) NOT NULL, rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), text TEXT NOT NULL, approved BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS slides ( id SERIAL PRIMARY KEY, title VARCHAR(255), subtitle TEXT, image_id INTEGER REFERENCES images(id) ON DELETE SET NULL, image_url VARCHAR(500), active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS room_comments ( id SERIAL PRIMARY KEY, room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, photo_url VARCHAR(500), user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, first_name VARCHAR(100) NOT NULL, last_initial VARCHAR(10), text TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), deleted_by_admin BOOLEAN DEFAULT FALSE ); `); await db.query(` CREATE TABLE IF NOT EXISTS social_events ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, price VARCHAR(50), date DATE, photos TEXT[] DEFAULT '{}', featured_photo VARCHAR(500), active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS social_event_reservations ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE, guests INTEGER NOT NULL, notes TEXT, status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')), created_at TIMESTAMP DEFAULT NOW(), UNIQUE(user_id, social_event_id) ); `); await db.query(`CREATE TABLE IF NOT EXISTS calendar_events ( id SERIAL PRIMARY KEY, date DATE NOT NULL UNIQUE, title VARCHAR(255) NOT NULL, type VARCHAR(100) NOT NULL, description TEXT, color VARCHAR(20) NOT NULL, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW() );`); await db.query(` CREATE TABLE IF NOT EXISTS user_sessions ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, token TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE; `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100); `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS preferred_language VARCHAR(10) DEFAULT 'es'; `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS terms_accepted_at TIMESTAMP; `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255); `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(50); `); await db.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS country VARCHAR(100); `); await db.query(` CREATE TABLE IF NOT EXISTS terms_content ( id SERIAL PRIMARY KEY, language VARCHAR(10) NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, version VARCHAR(20) NOT NULL DEFAULT '1.0', updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(language, version) ); `); await db.query(` CREATE TABLE IF NOT EXISTS availability ( id SERIAL PRIMARY KEY, room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, date DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'available' CHECK (status IN ('available', 'booked', 'maintenance')), price NUMERIC(10,2), created_at TIMESTAMP DEFAULT NOW(), UNIQUE(room_id, date) ); `); await db.query(` CREATE TABLE IF NOT EXISTS trips ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, notes TEXT, status VARCHAR(20) NOT NULL DEFAULT 'confirmed' CHECK (status IN ('pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled')), created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, sender_role VARCHAR(20) NOT NULL CHECK (sender_role IN ('user', 'admin')), message TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS room_amenities ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, icon_svg TEXT NOT NULL, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS room_amenity_links ( room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, amenity_id INTEGER REFERENCES room_amenities(id) ON DELETE CASCADE, PRIMARY KEY (room_id, amenity_id) ); `); await db.query(` CREATE TABLE IF NOT EXISTS reservations ( id SERIAL PRIMARY KEY, room_id INTEGER NOT NULL REFERENCES rooms(id), user_id INTEGER NOT NULL REFERENCES users(id), check_in DATE NOT NULL, check_out DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', total_price DECIMAL(10,2), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); `); await db.query(` CREATE TABLE IF NOT EXISTS room_availability ( id SERIAL PRIMARY KEY, room_id INTEGER NOT NULL REFERENCES rooms(id), date DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'available', reason TEXT, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(room_id, date) ); `); await db.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, room_id INTEGER REFERENCES rooms(id), user_id INTEGER NOT NULL REFERENCES users(id), message TEXT NOT NULL, read_by_admins BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() ); `); await db.query(`ALTER TABLE room_comments ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'approved'`); await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`); } export async function migrateFromLegacyPlaces() { await db.query(` UPDATE places SET photos = ARRAY[photo] WHERE photo IS NOT NULL AND (photos IS NULL OR photos = '{}'); `); await db.query(` ALTER TABLE places DROP COLUMN IF EXISTS photo; `); await db.query(` ALTER TABLE places ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); `); await db.query(` ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); `); } export async function seed() { if (process.env.NODE_ENV === 'production') { throw new Error('Seeding is disabled in production'); } const adminPassword = process.env.BOOTSTRAP_ADMIN_PASSWORD; const userPassword = process.env.BOOTSTRAP_USER_PASSWORD; const wifiPassword = process.env.BOOTSTRAP_WIFI_PASSWORD; const tagline = process.env.BOOTSTRAP_TAGLINE || 'Hotel · Restaurant · Andean Highlands'; const { rows: adminRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`); if (adminRows.length === 0 && adminPassword) { const adminHash = bcrypt.hashSync(adminPassword, 10); const seedRows: Array<[string, string, string]> = [['admin', adminHash, 'admin']]; if (userPassword) { seedRows.push(['user', bcrypt.hashSync(userPassword, 10), 'user']); } await db.query( `INSERT INTO users (username, password_hash, role) VALUES ${seedRows .map((_, index) => `($${index * 3 + 1}, $${index * 3 + 2}, $${index * 3 + 3})`) .join(', ')}`, seedRows.flat() ); } const { rows: offerRows } = await db.query(`SELECT id FROM offers`); if (offerRows.length === 0) { await db.query( `INSERT INTO offers (title, description) VALUES ($1, $2), ($3, $4)`, [ 'Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', 'Sunset Dinner', 'Three-course menu on the terrace every Friday.', ] ); } const { rows: benefitRows } = await db.query(`SELECT id FROM benefits`); if (benefitRows.length === 0) { await db.query( `INSERT INTO benefits (text) VALUES ($1), ($2), ($3), ($4)`, [ 'Priority booking for peak weekends', 'Late checkout on availability', 'Free welcome cocktail', 'Member-only events and tastings', ] ); } const { rows: configWifiRows } = await db.query(`SELECT key FROM config WHERE key = 'wifi_password'`); if (configWifiRows.length === 0 && wifiPassword) { await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['wifi_password', wifiPassword]); } const { rows: configTaglineRows } = await db.query(`SELECT key FROM config WHERE key = 'tagline'`); if (configTaglineRows.length === 0) { await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['tagline', tagline]); } const { rows: calendarRows } = await db.query(`SELECT id FROM calendar_events`); if (calendarRows.length === 0) { await db.query( `INSERT INTO calendar_events (date, title, type, description, color, active) VALUES ($1, $2, $3, $4, $5, $6)`, ['2026-06-30', 'Welcome Dinner', 'event', 'Andean tasting menu', '#b45309', true] ); } const { rows: socialRows } = await db.query(`SELECT id FROM social_events`); if (socialRows.length === 0) { await db.query( `INSERT INTO social_events (name, description, price, date, photos, featured_photo, active, sort_order) VALUES ($1, $2, $3, $4, '{}', null, $5, $6)`, ['Sunset Tasting', 'Local wines on the terrace.', 'USD 35', '2026-07-04', true, 1] ); } const { rows: socialReservationRows } = await db.query(`SELECT id FROM social_event_reservations`); if (socialReservationRows.length === 0) { const { rows: adminIdRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`); if (adminIdRows.length > 0) { await db.query( `INSERT INTO social_event_reservations (user_id, social_event_id, guests, notes, status) SELECT $1, id, $2, $3, $4 FROM social_events LIMIT 1`, [adminIdRows[0].id, 2, 'Anniversary', 'confirmed'] ); } } const { rows: amenityRows } = await db.query(`SELECT id FROM room_amenities`); if (amenityRows.length === 0) { const defaultAmenities = [ ['Wi-Fi', '', 1], ['TV', '', 2], ['Shower', '', 3], ['Bathtub', '', 4], ['Fireplace', '', 5], ['Minibar', '', 6], ['Coffee Maker', '', 7], ['Safe', '', 8], ['Heating', '', 9], ['Balcony', '', 10], ['King Bed', '', 11], ['Queen Bed', '', 13], ['2 Queen Beds', '', 14], ['Mountain View', '', 12], ]; for (const [name, icon_svg, sort_order] of defaultAmenities) { await db.query( 'INSERT INTO room_amenities (name, icon_svg, sort_order) VALUES ($1, $2, $3)', [name, icon_svg, sort_order] ); } } } export async function resetDatabase() { await db.query('DROP TABLE IF EXISTS room_comments, reviews, places, menu_items, rooms, config, benefits, offers, coupons, reservations, images, users, slides, calendar_events, social_events, social_event_reservations CASCADE'); await initSchema(); await migrateFromLegacyPlaces(); await seed(); }