import { Pool } from 'pg' const pool = new Pool({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, ssl: process.env.DATABASE_SSL === 'false' ? false : { rejectUnauthorized: false }, }) // ============================================ // Users // ============================================ export async function findUserByTelegramId(telegramId: number) { const { rows } = await pool.query( 'SELECT * FROM users WHERE telegram_id = $1', [telegramId] ) return rows[0] || null } export async function createUser(data: { telegram_id: number username?: string display_name?: string language_code?: string }) { const { rows } = await pool.query( `INSERT INTO users (telegram_id, username, display_name, language_code) VALUES ($1, $2, $3, $4) ON CONFLICT (telegram_id) DO UPDATE SET username = COALESCE($2, users.username), display_name = COALESCE($3, users.display_name), language_code = COALESCE($4, users.language_code), updated_at = NOW() RETURNING *`, [data.telegram_id, data.username || null, data.display_name || null, data.language_code || 'ru'] ) return rows[0] } // ============================================ // Cities // ============================================ export async function getActiveCities() { const { rows } = await pool.query( "SELECT * FROM cities WHERE status = 'active' ORDER BY name" ) return rows } export async function getCityById(id: string) { const { rows } = await pool.query('SELECT * FROM cities WHERE id = $1', [id]) return rows[0] || null } export async function getOrCreateCityByName(name: string) { const normalizedName = name.trim().replace(/\s+/g, ' ') const { rows: existing } = await pool.query( 'SELECT * FROM cities WHERE lower(name) = lower($1) LIMIT 1', [normalizedName] ) if (existing[0]) return existing[0] const { rows } = await pool.query( `INSERT INTO cities (name, country, description, status) VALUES ($1, $2, $3, 'active') RETURNING *`, [normalizedName, 'Unknown', 'User-added city'] ) return rows[0] } // ============================================ // Quests // ============================================ export async function getActiveQuestForUser(userId: string) { const { rows } = await pool.query( `SELECT q.*, c.name as city_name, c.country as city_country FROM quests q JOIN cities c ON q.city_id = c.id WHERE q.user_id = $1 AND q.status = 'in_progress' LIMIT 1`, [userId] ) return rows[0] || null } export async function createQuest(data: { city_id: string user_id: string title?: string description?: string number_of_days: number pace: string companions: string user_comment?: string }) { const { rows } = await pool.query( `INSERT INTO quests (city_id, user_id, title, description, number_of_days, pace, companions, user_comment) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, [ data.city_id, data.user_id, data.title || null, data.description || null, data.number_of_days, data.pace, data.companions, data.user_comment || null ] ) return rows[0] } export async function updateQuestStatus(questId: string, status: string) { const extra = status === 'in_progress' ? ", started_at = NOW()" : status === 'completed' ? ", completed_at = NOW()" : "" const { rows } = await pool.query( `UPDATE quests SET status = $1, updated_at = NOW()${extra} WHERE id = $2 RETURNING *`, [status, questId] ) return rows[0] || null } // ============================================ // Quest Days // ============================================ export async function createQuestDay(data: { quest_id: string day_number: number theme?: string }) { const { rows } = await pool.query( `INSERT INTO quest_days (quest_id, day_number, theme) VALUES ($1, $2, $3) RETURNING *`, [data.quest_id, data.day_number, data.theme || null] ) return rows[0] } export async function getQuestDays(questId: string) { const { rows } = await pool.query( 'SELECT * FROM quest_days WHERE quest_id = $1 ORDER BY day_number', [questId] ) return rows } export async function getActiveDayForQuest(questId: string) { const { rows } = await pool.query( `SELECT * FROM quest_days WHERE quest_id = $1 AND status = 'in_progress' LIMIT 1`, [questId] ) return rows[0] || null } export async function updateDayStatus(dayId: string, status: string) { const extra = status === 'in_progress' ? ", started_at = NOW()" : status === 'completed' ? ", completed_at = NOW()" : "" const { rows } = await pool.query( `UPDATE quest_days SET status = $1${extra} WHERE id = $2 RETURNING *`, [status, dayId] ) return rows[0] || null } // ============================================ // Quest Points // ============================================ export async function createQuestPoint(data: { day_id: string title: string location_lat?: number location_lon?: number teaser_text?: string content_text?: string order_in_day: number status?: string }) { const { rows } = await pool.query( `INSERT INTO quest_points (day_id, title, location_lat, location_lon, teaser_text, content_text, order_in_day, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (day_id, order_in_day) DO NOTHING RETURNING *`, [ data.day_id, data.title, data.location_lat || null, data.location_lon || null, data.teaser_text || null, data.content_text || null, data.order_in_day, data.status || 'hidden' ] ) return rows[0] || null } export async function getActivePointForDay(dayId: string) { const { rows } = await pool.query( `SELECT * FROM quest_points WHERE day_id = $1 AND status = 'active' LIMIT 1`, [dayId] ) return rows[0] || null } export async function getPointsForDay(dayId: string) { const { rows } = await pool.query( 'SELECT * FROM quest_points WHERE day_id = $1 ORDER BY order_in_day', [dayId] ) return rows } export async function getPointById(pointId: string) { const { rows } = await pool.query( 'SELECT * FROM quest_points WHERE id = $1', [pointId] ) return rows[0] || null } export async function updatePointStatus(pointId: string, status: string) { const extra = status === 'active' ? "" : status === 'completed' ? ", completed_at = NOW()" : "" const arrivedExtra = status === 'active' ? "" : "" const { rows } = await pool.query( `UPDATE quest_points SET status = $1${extra} WHERE id = $2 RETURNING *`, [status, pointId] ) return rows[0] || null } export async function markPointArrived(pointId: string) { const { rows } = await pool.query( `UPDATE quest_points SET arrived_at = NOW() WHERE id = $1 RETURNING *`, [pointId] ) return rows[0] || null } export async function updatePointContent(pointId: string, contentText: string) { const { rows } = await pool.query( `UPDATE quest_points SET content_text = $1 WHERE id = $2 RETURNING *`, [contentText, pointId] ) return rows[0] || null } // ============================================ // Quest Events // ============================================ export async function createQuestEvent(data: { quest_id: string user_id: string point_id?: string event_type: string payload?: Record }) { const { rows } = await pool.query( `INSERT INTO quest_events (quest_id, user_id, point_id, event_type, payload) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [data.quest_id, data.user_id, data.point_id || null, data.event_type, JSON.stringify(data.payload || {})] ) return rows[0] } export async function claimUnprocessedEvents(limit = 20) { const { rows } = await pool.query( `WITH next_events AS ( SELECT id FROM quest_events WHERE processed = false ORDER BY created_at ASC FOR UPDATE SKIP LOCKED LIMIT $1 ) UPDATE quest_events q SET processed = true FROM next_events n WHERE q.id = n.id RETURNING q.*`, [limit] ) return rows } // ============================================ // Chat History // ============================================ export async function saveChatMessage(data: { user_id: string quest_id: string point_id?: string message_type: string content: string }) { const { rows } = await pool.query( `INSERT INTO chat_history (user_id, quest_id, point_id, message_type, content) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [data.user_id, data.quest_id, data.point_id || null, data.message_type, data.content] ) return rows[0] } export async function getChatHistory(questId: string) { const { rows } = await pool.query( 'SELECT * FROM chat_history WHERE quest_id = $1 ORDER BY created_at', [questId] ) return rows } // ============================================ // Achievements // ============================================ export async function createAchievement(data: { user_id: string type: string city_id?: string }) { const { rows } = await pool.query( `INSERT INTO achievements (user_id, type, city_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type, city_id) DO NOTHING RETURNING *`, [data.user_id, data.type, data.city_id || null] ) return rows[0] || null } export async function getUserAchievements(userId: string) { const { rows } = await pool.query( 'SELECT * FROM achievements WHERE user_id = $1 ORDER BY achieved_at', [userId] ) return rows } // ============================================ // Full quest state (for API responses) // ============================================ export async function getFullQuestState(userId: string) { const quest = await getActiveQuestForUser(userId) if (!quest) return null const day = await getActiveDayForQuest(quest.id) if (!day) { return { quest, day: null, point: null, dayProgress: null } } const points = await getPointsForDay(day.id) const activePoint = points.find((p: { status: string }) => p.status === 'active') || null const completedCount = points.filter((p: { status: string }) => p.status === 'completed').length return { quest, day, point: activePoint, dayProgress: { totalPoints: points.length, completedPoints: completedCount, } } } export { pool }