import type { ApiRequest, ApiResponse } from '../_lib/types.js' import { cors, withTelegramAuth } from '../_lib/auth.js' import { questEventSchema } from '../_lib/validation.js' import { findUserByTelegramId, createQuestEvent, getPointById, updatePointStatus, markPointArrived, updatePointContent, getActivePointForDay, getActiveDayForQuest, getActiveQuestForUser, updateDayStatus, getPointsForDay, } from '../_lib/db.js' async function handler(req: ApiRequest, res: ApiResponse) { cors(req, res) if (req.method === 'OPTIONS') { return res.status(200).end() } if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }) } const telegramUser = req.telegramUser if (!telegramUser) { return res.status(401).json({ error: 'Not authenticated' }) } const parsed = questEventSchema.safeParse(req.body) if (!parsed.success) { return res.status(400).json({ error: 'Invalid request', details: parsed.error.issues }) } const { questId, pointId, eventType, payload } = parsed.data const user = await findUserByTelegramId(telegramUser.id) if (!user) { return res.status(404).json({ error: 'User not found' }) } // Immediate state updates for UI responsiveness let nextAction: string = 'wait' let updatedPoint = null let shouldCreateEvent = true const currentPoint = pointId ? await getPointById(pointId) : null switch (eventType) { case 'arrived': { if (pointId) { if (currentPoint?.arrived_at) { shouldCreateEvent = false updatedPoint = currentPoint } else { await markPointArrived(pointId) updatedPoint = await getPointById(pointId) } nextAction = 'show_content' } break } case 'point_completed': { if (pointId) { if (currentPoint?.status === 'completed' || currentPoint?.status === 'skipped') { shouldCreateEvent = false } else { await updatePointStatus(pointId, 'completed') } // Check if there are more points in the day const quest = await getActiveQuestForUser(user.id) if (quest) { const day = await getActiveDayForQuest(quest.id) if (day) { const points = await getPointsForDay(day.id) const nextPoint = points.find((p: { status: string }) => p.status === 'hidden' || p.status === 'preview') if (nextPoint) { nextAction = 'next_point' } else { nextAction = 'day_complete' } } } } break } case 'skipped': { if (pointId) { if (currentPoint?.status === 'completed' || currentPoint?.status === 'skipped') { shouldCreateEvent = false } else { await updatePointStatus(pointId, 'skipped') } nextAction = 'next_point' } break } case 'finished_today': { const quest = await getActiveQuestForUser(user.id) if (quest) { const day = await getActiveDayForQuest(quest.id) if (day) { // Mark all remaining points as skipped const points = await getPointsForDay(day.id) for (const point of points) { if (point.status !== 'completed' && point.status !== 'skipped') { await updatePointStatus(point.id, 'skipped') } } await updateDayStatus(day.id, 'completed') nextAction = 'day_complete' } } break } } // Write event to DB (bot will process it asynchronously) if (shouldCreateEvent) { await createQuestEvent({ quest_id: questId, user_id: user.id, point_id: pointId, event_type: eventType, payload, }) } return res.json({ success: true, updatedPoint, nextAction, }) } export default withTelegramAuth(handler)