FCS-dev
Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.
6112db7 | /** | |
| * Logica de negocio del modulo de alertas. | |
| * | |
| * Responsabilidades: | |
| * - list(userId, query) → devuelve alertas paginadas del usuario. | |
| * - processAll() → revisa la watchlist cada 60s (scheduler): | |
| * 1. Obtiene entradas de watchlist con alertThreshold. | |
| * 2. Si yesPrice >= alertThreshold y no hay alerta reciente (5 min dedup): | |
| * - Crea alerta en base de datos. | |
| * - Envía mensaje por Telegram (si hay bot token y chatId). | |
| * - Emite evento 'price_alert' por Socket.io. | |
| * | |
| * Tipos de alerta: | |
| * - price_threshold : umbral de precio cruzado. | |
| * - signal_change : cambio de senal IA (reservado para futuras versiones). | |
| * | |
| * Consumido por: | |
| * - alerts.controller.js (API REST). | |
| * - scheduler.js (processAlerts cada 60s). | |
| */ | |
| import { z } from 'zod'; | |
| import { alertsRepository } from './alerts.repository.js'; | |
| import { watchlistRepository } from '../watchlist/watchlist.repository.js'; | |
| import { sendMessage, formatPriceAlert } from './telegram.client.js'; | |
| import { emitPriceAlert } from '../socket/broadcaster.js'; | |
| import { logger } from '../utils/logger.js'; | |
| const listQuery = z.object({ | |
| limit: z.coerce.number().int().min(1).max(200).default(50), | |
| offset: z.coerce.number().int().min(0).default(0), | |
| }); | |
| const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 min | |
| export const alertsService = { | |
| list(userId, query) { | |
| const { limit, offset } = listQuery.parse(query); | |
| return alertsRepository.findByUser(userId, { limit, offset }); | |
| }, | |
| async processAll() { | |
| const entries = await watchlistRepository.findAllWithThreshold(); | |
| for (const entry of entries) { | |
| const { alertThreshold, user, market } = entry; | |
| if (!market.yesPrice || market.yesPrice < alertThreshold) continue; | |
| const recent = await alertsRepository.findRecent(user.id, market.id, 'price_threshold', DEDUP_WINDOW_MS); | |
| if (recent) continue; | |
| const message = formatPriceAlert(market.question, market.yesPrice, alertThreshold); | |
| await alertsRepository.create({ userId: user.id, marketId: market.id, type: 'price_threshold', message }); | |
| if (user.telegramAlertsEnabled) { | |
| await sendMessage({ botToken: user.telegramBotToken, chatId: user.telegramChatId, text: message }); | |
| } | |
| emitPriceAlert({ marketId: market.id, type: 'price_threshold', message }); | |
| logger.info({ marketId: market.id, userId: user.id }, 'price alert sent'); | |
| } | |
| }, | |
| }; | |