From 62a7773a76bdf90824f3df5c6b9702d3bcc01d0e Mon Sep 17 00:00:00 2001 From: ponzost Date: Tue, 24 Feb 2026 13:14:11 +0100 Subject: [PATCH] Alert module version 0.2.1 - Clean routes --- .../modules/alerts/adapters/mysql.adapter.js | 682 ++++++++-------- .../modules/alerts/alerts.controller.js | 734 +++++++++--------- Wallette/server/modules/alerts/alerts.repo.js | 282 +++---- .../server/modules/alerts/alerts.router.js | 160 ++-- .../server/modules/alerts/alerts.service.js | 646 +++++++-------- .../server/modules/alerts/channels/console.js | 466 +++++------ .../server/modules/alerts/channels/discord.js | 46 +- .../server/modules/alerts/channels/mailer.js | 452 +++++------ .../modules/alerts/channels/telegram.js | 110 +-- .../server/modules/alerts/channels/web.js | 152 ++-- Wallette/server/modules/alerts/index.js | 152 ++-- .../server/modules/alerts/socketManager.js | 388 ++++----- Wallette/server/modules/alerts/test-alerts.js | 328 ++++---- Wallette/server/modules/init-alerts.js | 268 +++---- 14 files changed, 2433 insertions(+), 2433 deletions(-) diff --git a/Wallette/server/modules/alerts/adapters/mysql.adapter.js b/Wallette/server/modules/alerts/adapters/mysql.adapter.js index 7236df1..830efbd 100644 --- a/Wallette/server/modules/alerts/adapters/mysql.adapter.js +++ b/Wallette/server/modules/alerts/adapters/mysql.adapter.js @@ -1,341 +1,341 @@ -// ========================================================= -// MYSQL ADAPTER - Couche d'accès MySQL -// ========================================================= -// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB -// ========================================================= -// Ce fichier isole le SQL du reste du module. -// Pour utiliser MongoDB, créer un mongo.adapter.js -// qui implémente les mêmes méthodes. -// ========================================================= - -import { v4 as uuidv4 } from 'uuid'; - -// ========================================================= -// FACTORY FUNCTION : Créer un adapter MySQL -// ========================================================= -/** - * Crée un adapter MySQL pour le module d'alertes - * - * @param {Object} dbConnection - Connexion MySQL (mysql2/promise) - * @param {Object} options - Configuration des noms de tables - * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules') - * @param {string} options.usersTable - Nom de la table users (défaut: 'users') - * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events') - * - * @returns {Object} Adapter avec toutes les méthodes d'accès DB - * - * @example - * import db from './config/db.js'; - * import { createMySQLAdapter } from './adapters/mysql.adapter.js'; - * - * const adapter = createMySQLAdapter(db); - * const rules = await adapter.findActiveRulesForSignal('user-123', 1); - */ -export function createMySQLAdapter(dbConnection, options = {}) { - - // ========================================================= - // CONFIGURATION DES NOMS DE TABLES - // ========================================================= - const tables = { - alerts: options.alertsTable || 'alert_rules', - users: options.usersTable || 'users', - events: options.eventsTable || 'alert_events' - }; - - console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`); - - // ========================================================= - // RETOURNER L'OBJET ADAPTER - // ========================================================= - return { - - // ===================================================== - // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES - // ===================================================== - async findActiveRulesForSignal(userId, pairId) { - try { - const sql = ` - SELECT - ar.*, - u.email - FROM ${tables.alerts} ar - JOIN ${tables.users} u ON ar.user_id = u.user_id - WHERE ar.enabled = 1 - AND ar.user_id = ? - AND (ar.pair_id = ? OR ar.pair_id IS NULL) - ORDER BY ar.severity DESC - `; - - const [rows] = await dbConnection.execute(sql, [userId, pairId]); - - console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`); - return rows; - - } catch (error) { - console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error); - throw error; - } - }, - - // ===================================================== - // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE - // ===================================================== - async saveAlertEvent(eventData) { - try { - const sql = ` - INSERT INTO ${tables.events} ( - alert_event_id, - rule_id, - timestamp_ms, - severity, - channel, - send_status, - title, - message, - signal_action, - confidence, - correlation_id, - created_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - - const values = [ - eventData.alert_event_id || uuidv4(), - eventData.rule_id, - eventData.timestamp_ms || Date.now(), - eventData.severity, - eventData.channel, - eventData.send_status, - eventData.title, - eventData.message, - eventData.signal_action, - eventData.confidence, - eventData.correlation_id || null, - Date.now() - ]; - - await dbConnection.execute(sql, values); - console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`); - - } catch (error) { - console.error('[MySQL] Erreur dans saveAlertEvent:', error); - throw error; - } - }, - - // ===================================================== - // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION - // ===================================================== - async updateLastNotified(ruleId, timestamp) { - try { - const sql = ` - UPDATE ${tables.alerts} - SET last_notified_at_ms = ?, - updated_at_ms = ? - WHERE rule_id = ? - `; - - await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]); - console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`); - - } catch (error) { - console.error('[MySQL] Erreur dans updateLastNotified:', error); - throw error; - } - }, - - // ===================================================== - // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES - // ===================================================== - async getAlertHistory(userId, limit = 50) { - try { - const sql = ` - SELECT - ae.*, - ar.channel, - ar.rule_type - FROM ${tables.events} ae - JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id - WHERE ar.user_id = ? - ORDER BY ae.timestamp_ms DESC - LIMIT ? - `; - - const [rows] = await dbConnection.execute(sql, [userId, limit]); - return rows; - - } catch (error) { - console.error('[MySQL] Erreur dans getAlertHistory:', error); - throw error; - } - }, - - // ===================================================== - // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) - // ===================================================== - /** - * Crée une nouvelle règle d'alerte - * @param {Object} ruleData - Données de la règle - * @returns {Promise} Règle créée avec son ID - */ - async createRule(ruleData) { - try { - const ruleId = ruleData.ruleId || uuidv4(); - const now = Date.now(); - - const sql = ` - INSERT INTO ${tables.alerts} ( - rule_id, - user_id, - pair_id, - enabled, - rule_type, - severity, - min_confidence, - channel, - params, - cooldown_ms, - created_at_ms, - updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - - const values = [ - ruleId, - ruleData.userId, - ruleData.pairId || null, - ruleData.enabled !== undefined ? ruleData.enabled : 1, - ruleData.ruleType || 'SIGNAL_THRESHOLD', - ruleData.severity || 'INFO', - ruleData.minConfidence || 0.7, - ruleData.channel || 'CONSOLE', - JSON.stringify(ruleData.params || {}), - ruleData.cooldownMs || 60000, - now, - now - ]; - - await dbConnection.execute(sql, values); - console.log(`[MySQL] Règle créée : ${ruleId}`); - - return { ruleId, ...ruleData, createdAt: now }; - - } catch (error) { - console.error('[MySQL] Erreur dans createRule:', error); - throw error; - } - }, - - // ===================================================== - // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) - // ===================================================== - /** - * Met à jour une règle existante - * @param {string} ruleId - ID de la règle - * @param {Object} updates - Champs à mettre à jour - * @returns {Promise} true si modifié - */ - async updateRule(ruleId, updates) { - try { - // Construire la requête dynamiquement - const fields = []; - const values = []; - - if (updates.minConfidence !== undefined) { - fields.push('min_confidence = ?'); - values.push(updates.minConfidence); - } - if (updates.enabled !== undefined) { - fields.push('enabled = ?'); - values.push(updates.enabled); - } - if (updates.channel !== undefined) { - fields.push('channel = ?'); - values.push(updates.channel); - } - if (updates.severity !== undefined) { - fields.push('severity = ?'); - values.push(updates.severity); - } - if (updates.cooldownMs !== undefined) { - fields.push('cooldown_ms = ?'); - values.push(updates.cooldownMs); - } - if (updates.pairId !== undefined) { - fields.push('pair_id = ?'); - values.push(updates.pairId); - } - - // Toujours mettre à jour le timestamp - fields.push('updated_at_ms = ?'); - values.push(Date.now()); - - // Ajouter le ruleId à la fin pour le WHERE - values.push(ruleId); - - const sql = ` - UPDATE ${tables.alerts} - SET ${fields.join(', ')} - WHERE rule_id = ? - `; - - const [result] = await dbConnection.execute(sql, values); - console.log(`[MySQL] Règle ${ruleId} mise à jour`); - - return result.affectedRows > 0; - - } catch (error) { - console.error('[MySQL] Erreur dans updateRule:', error); - throw error; - } - }, - - // ===================================================== - // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) - // ===================================================== - /** - * Supprime une règle - * @param {string} ruleId - ID de la règle - * @returns {Promise} true si supprimé - */ - async deleteRule(ruleId) { - try { - const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`; - - const [result] = await dbConnection.execute(sql, [ruleId]); - console.log(`[MySQL] Règle ${ruleId} supprimée`); - - return result.affectedRows > 0; - - } catch (error) { - console.error('[MySQL] Erreur dans deleteRule:', error); - throw error; - } - }, - - // ===================================================== - // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) - // ===================================================== - /** - * Récupère une règle par son ID - * @param {string} ruleId - ID de la règle - * @returns {Promise} La règle ou null - */ - async findRuleById(ruleId) { - try { - const sql = ` - SELECT * FROM ${tables.alerts} - WHERE rule_id = ? - `; - - const [rows] = await dbConnection.execute(sql, [ruleId]); - return rows.length > 0 ? rows[0] : null; - - } catch (error) { - console.error('[MySQL] Erreur dans findRuleById:', error); - throw error; - } - }, - }; -} - +// ========================================================= +// MYSQL ADAPTER - Couche d'accès MySQL +// ========================================================= +// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB +// ========================================================= +// Ce fichier isole le SQL du reste du module. +// Pour utiliser MongoDB, créer un mongo.adapter.js +// qui implémente les mêmes méthodes. +// ========================================================= + +import { v4 as uuidv4 } from 'uuid'; + +// ========================================================= +// FACTORY FUNCTION : Créer un adapter MySQL +// ========================================================= +/** + * Crée un adapter MySQL pour le module d'alertes + * + * @param {Object} dbConnection - Connexion MySQL (mysql2/promise) + * @param {Object} options - Configuration des noms de tables + * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules') + * @param {string} options.usersTable - Nom de la table users (défaut: 'users') + * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events') + * + * @returns {Object} Adapter avec toutes les méthodes d'accès DB + * + * @example + * import db from './config/db.js'; + * import { createMySQLAdapter } from './adapters/mysql.adapter.js'; + * + * const adapter = createMySQLAdapter(db); + * const rules = await adapter.findActiveRulesForSignal('user-123', 1); + */ +export function createMySQLAdapter(dbConnection, options = {}) { + + // ========================================================= + // CONFIGURATION DES NOMS DE TABLES + // ========================================================= + const tables = { + alerts: options.alertsTable || 'alert_rules', + users: options.usersTable || 'users', + events: options.eventsTable || 'alert_events' + }; + + console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`); + + // ========================================================= + // RETOURNER L'OBJET ADAPTER + // ========================================================= + return { + + // ===================================================== + // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES + // ===================================================== + async findActiveRulesForSignal(userId, pairId) { + try { + const sql = ` + SELECT + ar.*, + u.email + FROM ${tables.alerts} ar + JOIN ${tables.users} u ON ar.user_id = u.user_id + WHERE ar.enabled = 1 + AND ar.user_id = ? + AND (ar.pair_id = ? OR ar.pair_id IS NULL) + ORDER BY ar.severity DESC + `; + + const [rows] = await dbConnection.execute(sql, [userId, pairId]); + + console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`); + return rows; + + } catch (error) { + console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error); + throw error; + } + }, + + // ===================================================== + // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE + // ===================================================== + async saveAlertEvent(eventData) { + try { + const sql = ` + INSERT INTO ${tables.events} ( + alert_event_id, + rule_id, + timestamp_ms, + severity, + channel, + send_status, + title, + message, + signal_action, + confidence, + correlation_id, + created_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const values = [ + eventData.alert_event_id || uuidv4(), + eventData.rule_id, + eventData.timestamp_ms || Date.now(), + eventData.severity, + eventData.channel, + eventData.send_status, + eventData.title, + eventData.message, + eventData.signal_action, + eventData.confidence, + eventData.correlation_id || null, + Date.now() + ]; + + await dbConnection.execute(sql, values); + console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`); + + } catch (error) { + console.error('[MySQL] Erreur dans saveAlertEvent:', error); + throw error; + } + }, + + // ===================================================== + // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION + // ===================================================== + async updateLastNotified(ruleId, timestamp) { + try { + const sql = ` + UPDATE ${tables.alerts} + SET last_notified_at_ms = ?, + updated_at_ms = ? + WHERE rule_id = ? + `; + + await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]); + console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`); + + } catch (error) { + console.error('[MySQL] Erreur dans updateLastNotified:', error); + throw error; + } + }, + + // ===================================================== + // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES + // ===================================================== + async getAlertHistory(userId, limit = 50) { + try { + const sql = ` + SELECT + ae.*, + ar.channel, + ar.rule_type + FROM ${tables.events} ae + JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id + WHERE ar.user_id = ? + ORDER BY ae.timestamp_ms DESC + LIMIT ? + `; + + const [rows] = await dbConnection.execute(sql, [userId, limit]); + return rows; + + } catch (error) { + console.error('[MySQL] Erreur dans getAlertHistory:', error); + throw error; + } + }, + + // ===================================================== + // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) + // ===================================================== + /** + * Crée une nouvelle règle d'alerte + * @param {Object} ruleData - Données de la règle + * @returns {Promise} Règle créée avec son ID + */ + async createRule(ruleData) { + try { + const ruleId = ruleData.ruleId || uuidv4(); + const now = Date.now(); + + const sql = ` + INSERT INTO ${tables.alerts} ( + rule_id, + user_id, + pair_id, + enabled, + rule_type, + severity, + min_confidence, + channel, + params, + cooldown_ms, + created_at_ms, + updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const values = [ + ruleId, + ruleData.userId, + ruleData.pairId || null, + ruleData.enabled !== undefined ? ruleData.enabled : 1, + ruleData.ruleType || 'SIGNAL_THRESHOLD', + ruleData.severity || 'INFO', + ruleData.minConfidence || 0.7, + ruleData.channel || 'CONSOLE', + JSON.stringify(ruleData.params || {}), + ruleData.cooldownMs || 60000, + now, + now + ]; + + await dbConnection.execute(sql, values); + console.log(`[MySQL] Règle créée : ${ruleId}`); + + return { ruleId, ...ruleData, createdAt: now }; + + } catch (error) { + console.error('[MySQL] Erreur dans createRule:', error); + throw error; + } + }, + + // ===================================================== + // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) + // ===================================================== + /** + * Met à jour une règle existante + * @param {string} ruleId - ID de la règle + * @param {Object} updates - Champs à mettre à jour + * @returns {Promise} true si modifié + */ + async updateRule(ruleId, updates) { + try { + // Construire la requête dynamiquement + const fields = []; + const values = []; + + if (updates.minConfidence !== undefined) { + fields.push('min_confidence = ?'); + values.push(updates.minConfidence); + } + if (updates.enabled !== undefined) { + fields.push('enabled = ?'); + values.push(updates.enabled); + } + if (updates.channel !== undefined) { + fields.push('channel = ?'); + values.push(updates.channel); + } + if (updates.severity !== undefined) { + fields.push('severity = ?'); + values.push(updates.severity); + } + if (updates.cooldownMs !== undefined) { + fields.push('cooldown_ms = ?'); + values.push(updates.cooldownMs); + } + if (updates.pairId !== undefined) { + fields.push('pair_id = ?'); + values.push(updates.pairId); + } + + // Toujours mettre à jour le timestamp + fields.push('updated_at_ms = ?'); + values.push(Date.now()); + + // Ajouter le ruleId à la fin pour le WHERE + values.push(ruleId); + + const sql = ` + UPDATE ${tables.alerts} + SET ${fields.join(', ')} + WHERE rule_id = ? + `; + + const [result] = await dbConnection.execute(sql, values); + console.log(`[MySQL] Règle ${ruleId} mise à jour`); + + return result.affectedRows > 0; + + } catch (error) { + console.error('[MySQL] Erreur dans updateRule:', error); + throw error; + } + }, + + // ===================================================== + // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) + // ===================================================== + /** + * Supprime une règle + * @param {string} ruleId - ID de la règle + * @returns {Promise} true si supprimé + */ + async deleteRule(ruleId) { + try { + const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`; + + const [result] = await dbConnection.execute(sql, [ruleId]); + console.log(`[MySQL] Règle ${ruleId} supprimée`); + + return result.affectedRows > 0; + + } catch (error) { + console.error('[MySQL] Erreur dans deleteRule:', error); + throw error; + } + }, + + // ===================================================== + // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) + // ===================================================== + /** + * Récupère une règle par son ID + * @param {string} ruleId - ID de la règle + * @returns {Promise} La règle ou null + */ + async findRuleById(ruleId) { + try { + const sql = ` + SELECT * FROM ${tables.alerts} + WHERE rule_id = ? + `; + + const [rows] = await dbConnection.execute(sql, [ruleId]); + return rows.length > 0 ? rows[0] : null; + + } catch (error) { + console.error('[MySQL] Erreur dans findRuleById:', error); + throw error; + } + }, + }; +} + diff --git a/Wallette/server/modules/alerts/alerts.controller.js b/Wallette/server/modules/alerts/alerts.controller.js index ea02247..a153787 100644 --- a/Wallette/server/modules/alerts/alerts.controller.js +++ b/Wallette/server/modules/alerts/alerts.controller.js @@ -1,367 +1,367 @@ -// ========================================================= -// ALERTS CONTROLLER - VERSION RÉUTILISABLE -// ========================================================= -// RÔLE : Gestion des requêtes et réponses HTTP -// ========================================================= - - -/** - * Crée un controller d'alertes réutilisable - * - * @param {Object} alertsService - Service d'alertes (créé par createAlertsService) - * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes) - * - * @returns {Object} Controller avec toutes les méthodes - */ -export function createAlertsController(alertsService, alertsRepo) { - - console.log("Controller d'alertes initialisé"); - - return { - - // ===================================================== - // POST /api/alerts/process-signal - // ===================================================== - /** - * Endpoint pour traiter un signal crypto - * - * Appelé par le module Strategy (Sacha) quand il - * génère un signal BUY/SELL/HOLD - * - * Body attendu : - * { - * "userId": "uuid", - * "pairId": 1, - * "pair": "BTC/EUR", - * "action": "BUY", - * "confidence": 0.87, - * "criticality": "WARNING", - * "reason": "Indicators show...", - * "priceAtSignal": 45000.50 - * } - */ - async processSignal(req, res) { - try { - // ================================================================================== - // 1. RÉCUPÉRER LES DONNÉES DU BODY - // ================================================================================== - const signal = req.body; - - // ================================================================================== - // 2. VALIDATION DES DONNÉES - // ================================================================================== - // Vérifier que tous les champs obligatoires sont présents - const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality']; - /** - * on remplit le tableau des champs manquant ou faux du contenu de signal - */ - const missingFields = requiredFields.filter(field => !signal[field]); - // dans ce cas on retourne une erreur et le noms des champs en cause - if (missingFields.length > 0) { - return res.status(400).json({ - success: false, - error: 'Champs manquants', - missingFields: missingFields - }); - } - - // Valider le format de action - const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS']; - if (!validActions.includes(signal.action)) { - return res.status(400).json({ - success: false, - error: `Action invalide. Doit être: ${validActions.join(', ')}` - }); - } - - // Valider le format de criticality - const validCriticalities = ['CRITICAL', 'WARNING', 'INFO']; - if (!validCriticalities.includes(signal.criticality)) { - return res.status(400).json({ - success: false, - error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}` - }); - } - - // Valider la confidence (doit être entre 0 et 1) - if (signal.confidence < 0 || signal.confidence > 1) { - return res.status(400).json({ - success: false, - error: 'Confidence doit être entre 0 et 1' - }); - } - - // ================================================================================= - // 3. APPELER LE SERVICE (injecté) - // ================================================================================= - // Le service contient toute la logique - await alertsService.processSignal(signal); - - // ================================================================================= - // 4. RÉPONDRE AU CLIENT - // ================================================================================= - res.status(200).json({ - success: true, - message: 'Signal traité avec succès', - signal: { - action: signal.action, - pair: signal.pair, - confidence: signal.confidence - } - }); - - } catch (error) { - // ================================================================================= - // GESTION DES ERREURS - // ================================================================================= - console.error(' Erreur dans processSignal controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur lors du traitement du signal', - details: process.env.NODE_ENV === 'development' ? error.message : undefined - }); - } - }, - - // ===================================================== - // GET /api/alerts/rules/:userId - // ===================================================== - /** - * Récupère toutes les règles d'alerte d'un utilisateur - * - * Utile pour afficher la configuration dans l'interface - */ - async getRules(req, res) { - try { - const { userId } = req.params; - - // Validation - if (!userId) { - return res.status(400).json({ - success: false, - error: 'userId manquant' - }); - } - - // Récupérer les règles (via repo injecté) - const rules = await alertsRepo.findActiveRulesForSignal(userId, null); - - res.status(200).json({ - success: true, - count: rules.length, - rules: rules - }); - - } catch (error) { - console.error('Erreur dans getRules controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur' - }); - } - }, - - // ===================================================== - // GET /api/alerts/history/:userId - // ===================================================== - /** - * Récupère l'historique des alertes d'un utilisateur - */ - async getHistory(req, res) { - try { - const { userId } = req.params; - const limit = parseInt(req.query.limit) || 50; - - if (!userId) { - return res.status(400).json({ - success: false, - error: 'userId manquant' - }); - } - - // Récupérer l'historique (via repo injecté) - const history = await alertsRepo.getAlertHistory(userId, limit); - - res.status(200).json({ - success: true, - count: history.length, - history: history - }); - - } catch (error) { - console.error('Erreur dans getHistory controller:', error); - - res.status(500).json({ - success: false, - error: 'Erreur serveur' - }); - } - }, - - // ========================================================================================= - // CRUD - //========================================================================================== - - - // ===================================================== - // POST /api/alerts/rules - CRÉER UNE RÈGLE - // ===================================================== - /** - * Crée une nouvelle règle d'alerte - * - * Body attendu : - * { - * "userId": "uuid", - * "pairId": 1, - * "channel": "EMAIL", - * "minConfidence": 0.8, - * "severity": "WARNING", - * "cooldownMs": 60000 - * } - */ - async createRule(req, res) { - try { - const ruleData = req.body; - - // Validation des champs obligatoires - if (!ruleData.userId || !ruleData.channel) { - return res.status(400).json({ - success: false, - error: 'Champs manquants (userId, channel requis)' - }); - } - - // Appeler le repo (qui appelle l'adapter) - const newRule = await alertsRepo.createRule(ruleData); - - res.status(201).json({ - success: true, - rule: newRule, - message: 'Règle créée avec succès' - }); - - } catch (error) { - console.error('❌ Erreur createRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } - }, - - // ===================================================== - // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE - // ===================================================== - /** - * Met à jour une règle existante - * - * Body possible : - * { - * "minConfidence": 0.9, - * "enabled": true, - * "channel": "CONSOLE", - * "severity": "CRITICAL" - * } - */ - async updateRule(req, res) { - try { - const { id } = req.params; - const updates = req.body; - - // Vérifier qu'il y a quelque chose à mettre à jour - if (Object.keys(updates).length === 0) { - return res.status(400).json({ - success: false, - error: 'Aucune donnée à mettre à jour' - }); - } - - // Appeler le repo (qui appelle l'adapter) - const updated = await alertsRepo.updateRule(id, updates); - - if (!updated) { - return res.status(404).json({ - success: false, - error: 'Règle non trouvée' - }); - } - - res.json({ - success: true, - message: 'Règle mise à jour' - }); - - } catch (error) { - console.error('❌ Erreur updateRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } - }, - - // ===================================================== - // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE - // ===================================================== - async deleteRule(req, res) { - try { - const { id } = req.params; - - // Appeler le repo (qui appelle l'adapter) - const deleted = await alertsRepo.deleteRule(id); - - if (!deleted) { - return res.status(404).json({ - success: false, - error: 'Règle non trouvée' - }); - } - - res.json({ - success: true, - message: 'Règle supprimée' - }); - - } catch (error) { - console.error('❌ Erreur deleteRule:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } - }, - - // ===================================================== - // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE - // ===================================================== - async getRuleById(req, res) { - try { - const { id } = req.params; - - const rule = await alertsRepo.findRuleById(id); - - if (!rule) { - return res.status(404).json({ - success: false, - error: 'Règle non trouvée' - }); - } - - res.json({ - success: true, - rule - }); - - } catch (error) { - console.error('❌ Erreur getRuleById:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } - } - }; -} - +// ========================================================= +// ALERTS CONTROLLER - VERSION RÉUTILISABLE +// ========================================================= +// RÔLE : Gestion des requêtes et réponses HTTP +// ========================================================= + + +/** + * Crée un controller d'alertes réutilisable + * + * @param {Object} alertsService - Service d'alertes (créé par createAlertsService) + * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes) + * + * @returns {Object} Controller avec toutes les méthodes + */ +export function createAlertsController(alertsService, alertsRepo) { + + console.log("Controller d'alertes initialisé"); + + return { + + // ===================================================== + // POST /api/alerts/process-signal + // ===================================================== + /** + * Endpoint pour traiter un signal crypto + * + * Appelé par le module Strategy (Sacha) quand il + * génère un signal BUY/SELL/HOLD + * + * Body attendu : + * { + * "userId": "uuid", + * "pairId": 1, + * "pair": "BTC/EUR", + * "action": "BUY", + * "confidence": 0.87, + * "criticality": "WARNING", + * "reason": "Indicators show...", + * "priceAtSignal": 45000.50 + * } + */ + async processSignal(req, res) { + try { + // ================================================================================== + // 1. RÉCUPÉRER LES DONNÉES DU BODY + // ================================================================================== + const signal = req.body; + + // ================================================================================== + // 2. VALIDATION DES DONNÉES + // ================================================================================== + // Vérifier que tous les champs obligatoires sont présents + const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality']; + /** + * on remplit le tableau des champs manquant ou faux du contenu de signal + */ + const missingFields = requiredFields.filter(field => !signal[field]); + // dans ce cas on retourne une erreur et le noms des champs en cause + if (missingFields.length > 0) { + return res.status(400).json({ + success: false, + error: 'Champs manquants', + missingFields: missingFields + }); + } + + // Valider le format de action + const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS']; + if (!validActions.includes(signal.action)) { + return res.status(400).json({ + success: false, + error: `Action invalide. Doit être: ${validActions.join(', ')}` + }); + } + + // Valider le format de criticality + const validCriticalities = ['CRITICAL', 'WARNING', 'INFO']; + if (!validCriticalities.includes(signal.criticality)) { + return res.status(400).json({ + success: false, + error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}` + }); + } + + // Valider la confidence (doit être entre 0 et 1) + if (signal.confidence < 0 || signal.confidence > 1) { + return res.status(400).json({ + success: false, + error: 'Confidence doit être entre 0 et 1' + }); + } + + // ================================================================================= + // 3. APPELER LE SERVICE (injecté) + // ================================================================================= + // Le service contient toute la logique + await alertsService.processSignal(signal); + + // ================================================================================= + // 4. RÉPONDRE AU CLIENT + // ================================================================================= + res.status(200).json({ + success: true, + message: 'Signal traité avec succès', + signal: { + action: signal.action, + pair: signal.pair, + confidence: signal.confidence + } + }); + + } catch (error) { + // ================================================================================= + // GESTION DES ERREURS + // ================================================================================= + console.error(' Erreur dans processSignal controller:', error); + + res.status(500).json({ + success: false, + error: 'Erreur serveur lors du traitement du signal', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } + }, + + // ===================================================== + // GET /api/alerts/rules/:userId + // ===================================================== + /** + * Récupère toutes les règles d'alerte d'un utilisateur + * + * Utile pour afficher la configuration dans l'interface + */ + async getRules(req, res) { + try { + const { userId } = req.params; + + // Validation + if (!userId) { + return res.status(400).json({ + success: false, + error: 'userId manquant' + }); + } + + // Récupérer les règles (via repo injecté) + const rules = await alertsRepo.findActiveRulesForSignal(userId, null); + + res.status(200).json({ + success: true, + count: rules.length, + rules: rules + }); + + } catch (error) { + console.error('Erreur dans getRules controller:', error); + + res.status(500).json({ + success: false, + error: 'Erreur serveur' + }); + } + }, + + // ===================================================== + // GET /api/alerts/history/:userId + // ===================================================== + /** + * Récupère l'historique des alertes d'un utilisateur + */ + async getHistory(req, res) { + try { + const { userId } = req.params; + const limit = parseInt(req.query.limit) || 50; + + if (!userId) { + return res.status(400).json({ + success: false, + error: 'userId manquant' + }); + } + + // Récupérer l'historique (via repo injecté) + const history = await alertsRepo.getAlertHistory(userId, limit); + + res.status(200).json({ + success: true, + count: history.length, + history: history + }); + + } catch (error) { + console.error('Erreur dans getHistory controller:', error); + + res.status(500).json({ + success: false, + error: 'Erreur serveur' + }); + } + }, + + // ========================================================================================= + // CRUD + //========================================================================================== + + + // ===================================================== + // POST /api/alerts/rules - CRÉER UNE RÈGLE + // ===================================================== + /** + * Crée une nouvelle règle d'alerte + * + * Body attendu : + * { + * "userId": "uuid", + * "pairId": 1, + * "channel": "EMAIL", + * "minConfidence": 0.8, + * "severity": "WARNING", + * "cooldownMs": 60000 + * } + */ + async createRule(req, res) { + try { + const ruleData = req.body; + + // Validation des champs obligatoires + if (!ruleData.userId || !ruleData.channel) { + return res.status(400).json({ + success: false, + error: 'Champs manquants (userId, channel requis)' + }); + } + + // Appeler le repo (qui appelle l'adapter) + const newRule = await alertsRepo.createRule(ruleData); + + res.status(201).json({ + success: true, + rule: newRule, + message: 'Règle créée avec succès' + }); + + } catch (error) { + console.error('❌ Erreur createRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE + // ===================================================== + /** + * Met à jour une règle existante + * + * Body possible : + * { + * "minConfidence": 0.9, + * "enabled": true, + * "channel": "CONSOLE", + * "severity": "CRITICAL" + * } + */ + async updateRule(req, res) { + try { + const { id } = req.params; + const updates = req.body; + + // Vérifier qu'il y a quelque chose à mettre à jour + if (Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + error: 'Aucune donnée à mettre à jour' + }); + } + + // Appeler le repo (qui appelle l'adapter) + const updated = await alertsRepo.updateRule(id, updates); + + if (!updated) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + message: 'Règle mise à jour' + }); + + } catch (error) { + console.error('❌ Erreur updateRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE + // ===================================================== + async deleteRule(req, res) { + try { + const { id } = req.params; + + // Appeler le repo (qui appelle l'adapter) + const deleted = await alertsRepo.deleteRule(id); + + if (!deleted) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + message: 'Règle supprimée' + }); + + } catch (error) { + console.error('❌ Erreur deleteRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE + // ===================================================== + async getRuleById(req, res) { + try { + const { id } = req.params; + + const rule = await alertsRepo.findRuleById(id); + + if (!rule) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + rule + }); + + } catch (error) { + console.error('❌ Erreur getRuleById:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + } + }; +} + diff --git a/Wallette/server/modules/alerts/alerts.repo.js b/Wallette/server/modules/alerts/alerts.repo.js index 8c0e288..63cac34 100644 --- a/Wallette/server/modules/alerts/alerts.repo.js +++ b/Wallette/server/modules/alerts/alerts.repo.js @@ -1,141 +1,141 @@ -// ========================================================= -// ALERTS REPOSITORY - VERSION AVEC ADAPTER -// ========================================================= -// RÔLE : Interface entre le Service et l'Adapter -// ========================================================= -// Le repo ne fait PLUS de SQL directement. -// Il délègue à l'adapter injecté. -// ========================================================= - -// ========================================================= -// FACTORY FUNCTION : Créer un repository -// ========================================================= -/** - * Crée un repository d'alertes réutilisable - * - * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.) - * @returns {Object} Repository avec toutes les méthodes - * - * @example - * // Avec MySQL - * import { createMySQLAdapter } from './adapters/mysql.adapter.js'; - * import { createAlertsRepo } from './alerts.repo.js'; - * - * const adapter = createMySQLAdapter(db); - * const repo = createAlertsRepo(adapter); - * - * @example - * // Avec MongoDB (adapter à créer) - * import { createMongoAdapter } from './adapters/mongo.adapter.js'; - * - * const adapter = createMongoAdapter(mongoClient); - * const repo = createAlertsRepo(adapter); - */ -export function createAlertsRepo(adapter) { - - console.log(`Repository d'alertes initialisé`); - -// ========================================================= -// RETOURNER L'OBJET REPOSITORY -// ========================================================= - return { - - // ===================================================== - // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES - // ===================================================== - /** - * Récupère les règles d'alerte pour un signal donné - * @param {string} userId - ID de l'utilisateur - * @param {number} pairId - ID de la paire crypto - * @returns {Promise} Liste des règles actives - */ - async findActiveRulesForSignal(userId, pairId) { - return adapter.findActiveRulesForSignal(userId, pairId); - }, - - // ===================================================== - // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE - // ===================================================== - /** - * Enregistre qu'une alerte a été envoyée - * @param {Object} eventData - Données de l'événement - */ - async saveAlertEvent(eventData) { - return adapter.saveAlertEvent(eventData); - }, - - // ===================================================== - // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION - // ===================================================== - /** - * Met à jour le timestamp pour le cooldown (anti-spam) - * @param {string} ruleId - ID de la règle - * @param {number} timestamp - Timestamp en millisecondes - */ - async updateLastNotified(ruleId, timestamp) { - return adapter.updateLastNotified(ruleId, timestamp); - }, - - // ===================================================== - // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES - // ===================================================== - /** - * Récupère les dernières alertes d'un utilisateur - * @param {string} userId - ID de l'utilisateur - * @param {number} limit - Nombre max de résultats - * @returns {Promise} Historique des alertes - */ - async getAlertHistory(userId, limit = 50) { - return adapter.getAlertHistory(userId, limit); - }, - - // ===================================================== - // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) - // ===================================================== - /** - * Crée une nouvelle règle d'alerte - * @param {Object} ruleData - Données de la règle - * @returns {Promise} Règle créée - */ - async createRule(ruleData) { - return adapter.createRule(ruleData); - }, - - // ===================================================== - // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) - // ===================================================== - /** - * Met à jour une règle existante - * @param {string} ruleId - ID de la règle - * @param {Object} updates - Champs à mettre à jour - * @returns {Promise} true si modifié - */ - async updateRule(ruleId, updates) { - return adapter.updateRule(ruleId, updates); - }, - - // ===================================================== - // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) - // ===================================================== - /** - * Supprime une règle - * @param {string} ruleId - ID de la règle - * @returns {Promise} true si supprimé - */ - async deleteRule(ruleId) { - return adapter.deleteRule(ruleId); - }, - - // ===================================================== - // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) - // ===================================================== - /** - * Récupère une règle par son ID - * @param {string} ruleId - ID de la règle - * @returns {Promise} La règle ou null - */ - async findRuleById(ruleId) { - return adapter.findRuleById(ruleId); - }, - }; -} +// ========================================================= +// ALERTS REPOSITORY - VERSION AVEC ADAPTER +// ========================================================= +// RÔLE : Interface entre le Service et l'Adapter +// ========================================================= +// Le repo ne fait PLUS de SQL directement. +// Il délègue à l'adapter injecté. +// ========================================================= + +// ========================================================= +// FACTORY FUNCTION : Créer un repository +// ========================================================= +/** + * Crée un repository d'alertes réutilisable + * + * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.) + * @returns {Object} Repository avec toutes les méthodes + * + * @example + * // Avec MySQL + * import { createMySQLAdapter } from './adapters/mysql.adapter.js'; + * import { createAlertsRepo } from './alerts.repo.js'; + * + * const adapter = createMySQLAdapter(db); + * const repo = createAlertsRepo(adapter); + * + * @example + * // Avec MongoDB (adapter à créer) + * import { createMongoAdapter } from './adapters/mongo.adapter.js'; + * + * const adapter = createMongoAdapter(mongoClient); + * const repo = createAlertsRepo(adapter); + */ +export function createAlertsRepo(adapter) { + + console.log(`Repository d'alertes initialisé`); + +// ========================================================= +// RETOURNER L'OBJET REPOSITORY +// ========================================================= + return { + + // ===================================================== + // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES + // ===================================================== + /** + * Récupère les règles d'alerte pour un signal donné + * @param {string} userId - ID de l'utilisateur + * @param {number} pairId - ID de la paire crypto + * @returns {Promise} Liste des règles actives + */ + async findActiveRulesForSignal(userId, pairId) { + return adapter.findActiveRulesForSignal(userId, pairId); + }, + + // ===================================================== + // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE + // ===================================================== + /** + * Enregistre qu'une alerte a été envoyée + * @param {Object} eventData - Données de l'événement + */ + async saveAlertEvent(eventData) { + return adapter.saveAlertEvent(eventData); + }, + + // ===================================================== + // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION + // ===================================================== + /** + * Met à jour le timestamp pour le cooldown (anti-spam) + * @param {string} ruleId - ID de la règle + * @param {number} timestamp - Timestamp en millisecondes + */ + async updateLastNotified(ruleId, timestamp) { + return adapter.updateLastNotified(ruleId, timestamp); + }, + + // ===================================================== + // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES + // ===================================================== + /** + * Récupère les dernières alertes d'un utilisateur + * @param {string} userId - ID de l'utilisateur + * @param {number} limit - Nombre max de résultats + * @returns {Promise} Historique des alertes + */ + async getAlertHistory(userId, limit = 50) { + return adapter.getAlertHistory(userId, limit); + }, + + // ===================================================== + // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) + // ===================================================== + /** + * Crée une nouvelle règle d'alerte + * @param {Object} ruleData - Données de la règle + * @returns {Promise} Règle créée + */ + async createRule(ruleData) { + return adapter.createRule(ruleData); + }, + + // ===================================================== + // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) + // ===================================================== + /** + * Met à jour une règle existante + * @param {string} ruleId - ID de la règle + * @param {Object} updates - Champs à mettre à jour + * @returns {Promise} true si modifié + */ + async updateRule(ruleId, updates) { + return adapter.updateRule(ruleId, updates); + }, + + // ===================================================== + // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) + // ===================================================== + /** + * Supprime une règle + * @param {string} ruleId - ID de la règle + * @returns {Promise} true si supprimé + */ + async deleteRule(ruleId) { + return adapter.deleteRule(ruleId); + }, + + // ===================================================== + // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) + // ===================================================== + /** + * Récupère une règle par son ID + * @param {string} ruleId - ID de la règle + * @returns {Promise} La règle ou null + */ + async findRuleById(ruleId) { + return adapter.findRuleById(ruleId); + }, + }; +} diff --git a/Wallette/server/modules/alerts/alerts.router.js b/Wallette/server/modules/alerts/alerts.router.js index 8280d86..bc7ac78 100644 --- a/Wallette/server/modules/alerts/alerts.router.js +++ b/Wallette/server/modules/alerts/alerts.router.js @@ -1,80 +1,80 @@ -// ========================================================= -// ALERTS ROUTER - VERSION RÉUTILISABLE -// ========================================================= -// RÔLE : Définir les routes (endpoints) de l'API -// ========================================================= - - -import express from 'express'; - -// ========================================================= -// FACTORY FUNCTION : Créer un router -// ========================================================= -/** - * Crée un router d'alertes réutilisable - * - * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController) - * - * @returns {express.Router} Router Express configuré - */ -export function createAlertsRouter(alertsController) { - - const router = express.Router(); - - console.log("Router d'alertes initialisé"); - -// ========================================================= - // DÉFINITION DES ROUTES -// ========================================================= - // POST /api/alerts/process-signal -// ========================================================= - /** - * Traiter un signal crypto et envoyer les alertes - * - * Exemple d'appel : - * POST http://localhost:3000/api/alerts/process-signal - * Body : { - * "userId": "user-123", - * "pairId": 1, - * "pair": "BTC/EUR", - * "action": "BUY", - * "confidence": 0.85, - * "criticality": "WARNING", - * "reason": "RSI oversold + MACD crossover" - * } - */ - router.post('/process-signal', alertsController.processSignal); - -// ============================================================================= - // GET /api/alerts/rules/:userId -// ============================================================================= - /** - * Récupérer les règles d'alerte d'un utilisateur - * - * Exemple d'appel : - * GET http://localhost:3000/api/alerts/rules/user-123 - */ - router.get('/rules/:userId', alertsController.getRules); - -// ============================================================================= - // GET /api/alerts/history/:userId -// ============================================================================= - /** - * Récupérer l'historique des alertes d'un utilisateur - * - * Exemple d'appel : - * GET http://localhost:3000/api/alerts/history/user-123?limit=20 - */ - router.get('/history/:userId', alertsController.getHistory); - -// ============================================================================= -// CRUD -// ============================================================================= - router.post('/rules', alertsController.createRule); - router.get('/rules/detail/:id', alertsController.getRuleById); - router.put('/rules/:id', alertsController.updateRule); - router.delete('/rules/:id', alertsController.deleteRule); - - return router; -} - +// ========================================================= +// ALERTS ROUTER - VERSION RÉUTILISABLE +// ========================================================= +// RÔLE : Définir les routes (endpoints) de l'API +// ========================================================= + + +import express from 'express'; + +// ========================================================= +// FACTORY FUNCTION : Créer un router +// ========================================================= +/** + * Crée un router d'alertes réutilisable + * + * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController) + * + * @returns {express.Router} Router Express configuré + */ +export function createAlertsRouter(alertsController) { + + const router = express.Router(); + + console.log("Router d'alertes initialisé"); + +// ========================================================= + // DÉFINITION DES ROUTES +// ========================================================= + // POST /api/alerts/process-signal +// ========================================================= + /** + * Traiter un signal crypto et envoyer les alertes + * + * Exemple d'appel : + * POST http://localhost:3000/api/alerts/process-signal + * Body : { + * "userId": "user-123", + * "pairId": 1, + * "pair": "BTC/EUR", + * "action": "BUY", + * "confidence": 0.85, + * "criticality": "WARNING", + * "reason": "RSI oversold + MACD crossover" + * } + */ + router.post('/process-signal', alertsController.processSignal); + +// ============================================================================= + // GET /api/alerts/rules/:userId +// ============================================================================= + /** + * Récupérer les règles d'alerte d'un utilisateur + * + * Exemple d'appel : + * GET http://localhost:3000/api/alerts/rules/user-123 + */ + router.get('/rules/:userId', alertsController.getRules); + +// ============================================================================= + // GET /api/alerts/history/:userId +// ============================================================================= + /** + * Récupérer l'historique des alertes d'un utilisateur + * + * Exemple d'appel : + * GET http://localhost:3000/api/alerts/history/user-123?limit=20 + */ + router.get('/history/:userId', alertsController.getHistory); + +// ============================================================================= +// CRUD +// ============================================================================= + router.post('/rules', alertsController.createRule); + router.get('/rules/detail/:id', alertsController.getRuleById); + router.put('/rules/:id', alertsController.updateRule); + router.delete('/rules/:id', alertsController.deleteRule); + + return router; +} + diff --git a/Wallette/server/modules/alerts/alerts.service.js b/Wallette/server/modules/alerts/alerts.service.js index 4280614..8df13fb 100644 --- a/Wallette/server/modules/alerts/alerts.service.js +++ b/Wallette/server/modules/alerts/alerts.service.js @@ -1,323 +1,323 @@ -// ========================================================= -// ALERTS SERVICE - VERSION RÉUTILISABLE -// ========================================================= -// RÔLE : Logique métier du système d'alertes -// ========================================================= -// PRINCIPE : INJECTION DE DÉPENDANCES -// Au lieu d'importer le Repository directement, on le reçoit en paramètre -// ========================================================= - -import { v4 as uuidv4 } from 'uuid'; - -// Import des canaux -import { sendAlertEmail } from './channels/mailer.js'; -import { sendConsoleAlert } from './channels/console.js'; -import { sendTelegramAlert } from './channels/telegram.js'; -import { sendDiscordAlert } from './channels/discord.js'; -import { sendWebAlert } from './channels/web.js'; - -// ========================================================= -// FACTORY FUNCTION : Créer un service -// ========================================================= -/** - * Crée un service d'alertes réutilisable - * - * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo) - * @param {Object} options - Configuration optionnelle - * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min) - * - * @returns {Object} Service avec toutes les méthodes - * - * @example - * // Dans Wall-e-tte - * import db from './config/db.js'; - * import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; - * import { createAlertsService } from './modules/alerts/alerts.service.js'; - * - * const repo = createAlertsRepo(db); - * const service = createAlertsService(repo); - * - * await service.processSignal(signal); - * - * @example - * // Dans un autre projet - * const repo = createAlertsRepo(strangersDb, { ... }); - * const service = createAlertsService(repo); - * await service.processSignal(signal); // Marche pareil ! - */ -export function createAlertsService(alertsRepo, options = {}) { - - console.log('🔧 Service d\'alertes initialisé'); - - // ───────────────────────────────────────────────────── - // RETOURNER L'OBJET SERVICE - // ───────────────────────────────────────────────────── - return { - - // ===================================================== - // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE - // ===================================================== - /** - * Détermine si une alerte doit être envoyée selon la règle - * - * Cette fonction applique les RÈGLES MÉTIER : - * - Vérification de la confidence minimum - * - Vérification du cooldown (anti-spam) - * - * @param {Object} rule - Règle d'alerte - * @param {Object} signal - Signal reçu - * @returns {boolean} true si l'alerte doit être envoyée - */ - shouldSendAlert(rule, signal) { - console.log(`Vérification règle ${rule.rule_id}...`); - - // ───────────────────────────────────────────────── - // CRITÈRE 1 : Vérifier la confidence minimum - // ───────────────────────────────────────────────── - // Si le signal a une confidence trop basse, on n'envoie pas - // Exemple : rule.min_confidence = 0.80 (80%) - // signal.confidence = 0.75 (75%) - // → Ne pas envoyer (75% < 80%) - - if (signal.confidence < rule.min_confidence) { - console.log(`Confidence trop basse`); - console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`); - console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`); - return false; - } - - // ───────────────────────────────────────────────── - // CRITÈRE 2 : Vérifier le cooldown (anti-spam) - // ───────────────────────────────────────────────── - // Le cooldown évite d'envoyer 100 emails en 1 minute - // Exemple : cooldown_ms = 60000 (1 minute) - // Dernière alerte envoyée il y a 30 secondes - // → Trop tôt, ne pas envoyer - - const now = Date.now(); - - if (rule.last_notified_at_ms) { - // Calculer le temps écoulé depuis la dernière alerte - const timeSinceLastAlert = now - rule.last_notified_at_ms; - - // Si pas assez de temps écoulé, ne pas envoyer - if (timeSinceLastAlert < rule.cooldown_ms) { - const remainingMs = rule.cooldown_ms - timeSinceLastAlert; - const remainingSec = Math.ceil(remainingMs / 1000); - - console.log(` ⏳ Cooldown actif`); - console.log(` Attendre encore ${remainingSec}s`); - return false; - } - } - - // ───────────────────────────────────────────────── - // TOUS LES CRITÈRES SONT OK ! - // ───────────────────────────────────────────────── - console.log(` Tous les critères sont remplis`); - return true; - }, - - // ===================================================== - // 2. ENVOYER UNE ALERTE VIA LE BON CANAL - // ===================================================== - /** - * Envoie une alerte via le canal spécifié dans la règle - * - * Cette fonction : - * - Choisit la bonne fonction d'envoi (email, telegram, etc.) - * - Prépare les paramètres spécifiques au canal - * - Gère les erreurs d'envoi - * - * @param {Object} rule - Règle d'alerte avec le canal - * @param {Object} signal - Signal à envoyer - * @returns {Promise} 'SENT' ou 'FAILED' - */ - async sendViaChannel(rule, signal) { - console.log(`Envoi via ${rule.channel}...`); - - let status = 'SENT'; - - try { - // ───────────────────────────────────────────── - // SWITCH selon le canal configuré - // ───────────────────────────────────────────── - switch (rule.channel) { - - // ── EMAIL ──────────────────────────────── - case 'EMAIL': - // Le destinataire est l'email de l'utilisateur - // On l'a récupéré via le JOIN dans la requête SQL - await sendAlertEmail(rule.email, signal); - break; - - // ── TELEGRAM ───────────────────────────── - case 'TELEGRAM': - // Le chatId est stocké dans rule.params (JSON) - // Exemple de params : {"chatId": "123456789"} - const params = JSON.parse(rule.params); - const chatId = params.chatId; - - if (!chatId) { - console.error(' Pas de chatId dans params'); - status = 'FAILED'; - break; - } - - await sendTelegramAlert(chatId, signal); - break; - - // ── DISCORD ────────────────────────────── - case 'DISCORD': - const discordParams = JSON.parse(rule.params); - const webhookUrl = discordParams.webhookUrl; - - if (!webhookUrl) { - console.error(' Pas de webhookUrl dans params'); - status = 'FAILED'; - break; - } - - await sendDiscordAlert(webhookUrl, signal); - break; - - // ── CONSOLE ────────────────────────────── - case 'CONSOLE': - // Pas de paramètres nécessaires - await sendConsoleAlert(signal); - break; - - // ── WEB ────────────────────────────────── - case 'WEB': - // Envoyer à l'interface web de l'utilisateur - await sendWebAlert(rule.user_id, signal); - break; - - // ── CANAL INCONNU ──────────────────────── - default: - console.warn(` Canal inconnu: ${rule.channel}`); - status = 'FAILED'; - } - - } catch (error) { - // ───────────────────────────────────────────── - // GESTION DES ERREURS - // ───────────────────────────────────────────── - console.error(` Erreur lors de l'envoi via ${rule.channel}:`); - console.error(` ${error.message}`); - status = 'FAILED'; - } - - return status; - }, - - // ===================================================== - // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES - // ===================================================== - /** - * Fonction principale qui traite un signal crypto - * - * WORKFLOW : - * 1. Récupérer les règles actives pour ce signal - * 2. Pour chaque règle : - * a. Vérifier si alerte doit être envoyée - * b. Envoyer via le bon canal - * c. Enregistrer dans l'historique - * d. Mettre à jour le timestamp - * - * @param {Object} signal - Signal crypto reçu - * @param {string} signal.userId - ID utilisateur - * @param {number} signal.pairId - ID de la paire - * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS - * @param {number} signal.confidence - 0-1 - * @param {string} signal.criticality - CRITICAL, WARNING, INFO - * @param {string} signal.reason - Explication - */ - async processSignal(signal) { - console.log('\n' + '═'.repeat(80)); - console.log(` TRAITEMENT DU SIGNAL`); - console.log(` Action : ${signal.action}`); - console.log(` Paire : ${signal.pair}`); - console.log(` User : ${signal.userId}`); - console.log('═'.repeat(80) + '\n'); - - try { - // ───────────────────────────────────────────── - // ÉTAPE 1 : Récupérer les règles actives - // ───────────────────────────────────────────── - // Utilise le repository injecté (pas d'import hardcodé) - const rules = await alertsRepo.findActiveRulesForSignal( - signal.userId, - signal.pairId - ); - - if (rules.length === 0) { - console.log(' Aucune règle active trouvée pour ce signal'); - console.log(' → Vérifie que des règles sont configurées en DB\n'); - return; - } - - console.log(` ${rules.length} règle(s) trouvée(s)\n`); - - // ───────────────────────────────────────────── - // ÉTAPE 2 : Traiter chaque règle - // ───────────────────────────────────────────── - let sentCount = 0; - - for (const rule of rules) { - console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`); - - // Vérifier si on doit envoyer - if (!this.shouldSendAlert(rule, signal)) { - console.log(`└─ Alerte NON envoyée\n`); - continue; - } - - // Envoyer via le canal - const status = await this.sendViaChannel(rule, signal); - - // Préparer les données de l'événement - const eventData = { - alert_event_id: uuidv4(), - rule_id: rule.rule_id, - timestamp_ms: Date.now(), - severity: signal.criticality, - channel: rule.channel, - send_status: status, - title: `${signal.action} ${signal.pair}`, - message: signal.reason, - signal_action: signal.action, - confidence: signal.confidence, - correlation_id: signal.correlationId || null - }; - - // Sauvegarder dans alert_events (via repo injecté) - await alertsRepo.saveAlertEvent(eventData); - - // Mettre à jour last_notified_at si envoi réussi (via repo injecté) - if (status === 'SENT') { - await alertsRepo.updateLastNotified(rule.rule_id, Date.now()); - sentCount++; - console.log(`└─ Alerte envoyée avec succès\n`); - } else { - console.log(`└─ Échec de l'envoi\n`); - } - } - - // ───────────────────────────────────────────── - // RÉSUMÉ FINAL - // ───────────────────────────────────────────── - console.log('═'.repeat(80)); - console.log(` RÉSUMÉ`); - console.log(` Règles vérifiées : ${rules.length}`); - console.log(` Alertes envoyées : ${sentCount}`); - console.log('═'.repeat(80) + '\n'); - - } catch (error) { - console.error(' ERREUR CRITIQUE dans processSignal:'); - console.error(error); - throw error; // Remonter l'erreur pour que le controller puisse la gérer - } - } - }; -} +// ========================================================= +// ALERTS SERVICE - VERSION RÉUTILISABLE +// ========================================================= +// RÔLE : Logique métier du système d'alertes +// ========================================================= +// PRINCIPE : INJECTION DE DÉPENDANCES +// Au lieu d'importer le Repository directement, on le reçoit en paramètre +// ========================================================= + +import { v4 as uuidv4 } from 'uuid'; + +// Import des canaux +import { sendAlertEmail } from './channels/mailer.js'; +import { sendConsoleAlert } from './channels/console.js'; +import { sendTelegramAlert } from './channels/telegram.js'; +import { sendDiscordAlert } from './channels/discord.js'; +import { sendWebAlert } from './channels/web.js'; + +// ========================================================= +// FACTORY FUNCTION : Créer un service +// ========================================================= +/** + * Crée un service d'alertes réutilisable + * + * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo) + * @param {Object} options - Configuration optionnelle + * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min) + * + * @returns {Object} Service avec toutes les méthodes + * + * @example + * // Dans Wall-e-tte + * import db from './config/db.js'; + * import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; + * import { createAlertsService } from './modules/alerts/alerts.service.js'; + * + * const repo = createAlertsRepo(db); + * const service = createAlertsService(repo); + * + * await service.processSignal(signal); + * + * @example + * // Dans un autre projet + * const repo = createAlertsRepo(strangersDb, { ... }); + * const service = createAlertsService(repo); + * await service.processSignal(signal); // Marche pareil ! + */ +export function createAlertsService(alertsRepo, options = {}) { + + console.log('🔧 Service d\'alertes initialisé'); + + // ───────────────────────────────────────────────────── + // RETOURNER L'OBJET SERVICE + // ───────────────────────────────────────────────────── + return { + + // ===================================================== + // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE + // ===================================================== + /** + * Détermine si une alerte doit être envoyée selon la règle + * + * Cette fonction applique les RÈGLES MÉTIER : + * - Vérification de la confidence minimum + * - Vérification du cooldown (anti-spam) + * + * @param {Object} rule - Règle d'alerte + * @param {Object} signal - Signal reçu + * @returns {boolean} true si l'alerte doit être envoyée + */ + shouldSendAlert(rule, signal) { + console.log(`Vérification règle ${rule.rule_id}...`); + + // ───────────────────────────────────────────────── + // CRITÈRE 1 : Vérifier la confidence minimum + // ───────────────────────────────────────────────── + // Si le signal a une confidence trop basse, on n'envoie pas + // Exemple : rule.min_confidence = 0.80 (80%) + // signal.confidence = 0.75 (75%) + // → Ne pas envoyer (75% < 80%) + + if (signal.confidence < rule.min_confidence) { + console.log(`Confidence trop basse`); + console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`); + console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`); + return false; + } + + // ───────────────────────────────────────────────── + // CRITÈRE 2 : Vérifier le cooldown (anti-spam) + // ───────────────────────────────────────────────── + // Le cooldown évite d'envoyer 100 emails en 1 minute + // Exemple : cooldown_ms = 60000 (1 minute) + // Dernière alerte envoyée il y a 30 secondes + // → Trop tôt, ne pas envoyer + + const now = Date.now(); + + if (rule.last_notified_at_ms) { + // Calculer le temps écoulé depuis la dernière alerte + const timeSinceLastAlert = now - rule.last_notified_at_ms; + + // Si pas assez de temps écoulé, ne pas envoyer + if (timeSinceLastAlert < rule.cooldown_ms) { + const remainingMs = rule.cooldown_ms - timeSinceLastAlert; + const remainingSec = Math.ceil(remainingMs / 1000); + + console.log(` ⏳ Cooldown actif`); + console.log(` Attendre encore ${remainingSec}s`); + return false; + } + } + + // ───────────────────────────────────────────────── + // TOUS LES CRITÈRES SONT OK ! + // ───────────────────────────────────────────────── + console.log(` Tous les critères sont remplis`); + return true; + }, + + // ===================================================== + // 2. ENVOYER UNE ALERTE VIA LE BON CANAL + // ===================================================== + /** + * Envoie une alerte via le canal spécifié dans la règle + * + * Cette fonction : + * - Choisit la bonne fonction d'envoi (email, telegram, etc.) + * - Prépare les paramètres spécifiques au canal + * - Gère les erreurs d'envoi + * + * @param {Object} rule - Règle d'alerte avec le canal + * @param {Object} signal - Signal à envoyer + * @returns {Promise} 'SENT' ou 'FAILED' + */ + async sendViaChannel(rule, signal) { + console.log(`Envoi via ${rule.channel}...`); + + let status = 'SENT'; + + try { + // ───────────────────────────────────────────── + // SWITCH selon le canal configuré + // ───────────────────────────────────────────── + switch (rule.channel) { + + // ── EMAIL ──────────────────────────────── + case 'EMAIL': + // Le destinataire est l'email de l'utilisateur + // On l'a récupéré via le JOIN dans la requête SQL + await sendAlertEmail(rule.email, signal); + break; + + // ── TELEGRAM ───────────────────────────── + case 'TELEGRAM': + // Le chatId est stocké dans rule.params (JSON) + // Exemple de params : {"chatId": "123456789"} + const params = JSON.parse(rule.params); + const chatId = params.chatId; + + if (!chatId) { + console.error(' Pas de chatId dans params'); + status = 'FAILED'; + break; + } + + await sendTelegramAlert(chatId, signal); + break; + + // ── DISCORD ────────────────────────────── + case 'DISCORD': + const discordParams = JSON.parse(rule.params); + const webhookUrl = discordParams.webhookUrl; + + if (!webhookUrl) { + console.error(' Pas de webhookUrl dans params'); + status = 'FAILED'; + break; + } + + await sendDiscordAlert(webhookUrl, signal); + break; + + // ── CONSOLE ────────────────────────────── + case 'CONSOLE': + // Pas de paramètres nécessaires + await sendConsoleAlert(signal); + break; + + // ── WEB ────────────────────────────────── + case 'WEB': + // Envoyer à l'interface web de l'utilisateur + await sendWebAlert(rule.user_id, signal); + break; + + // ── CANAL INCONNU ──────────────────────── + default: + console.warn(` Canal inconnu: ${rule.channel}`); + status = 'FAILED'; + } + + } catch (error) { + // ───────────────────────────────────────────── + // GESTION DES ERREURS + // ───────────────────────────────────────────── + console.error(` Erreur lors de l'envoi via ${rule.channel}:`); + console.error(` ${error.message}`); + status = 'FAILED'; + } + + return status; + }, + + // ===================================================== + // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES + // ===================================================== + /** + * Fonction principale qui traite un signal crypto + * + * WORKFLOW : + * 1. Récupérer les règles actives pour ce signal + * 2. Pour chaque règle : + * a. Vérifier si alerte doit être envoyée + * b. Envoyer via le bon canal + * c. Enregistrer dans l'historique + * d. Mettre à jour le timestamp + * + * @param {Object} signal - Signal crypto reçu + * @param {string} signal.userId - ID utilisateur + * @param {number} signal.pairId - ID de la paire + * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS + * @param {number} signal.confidence - 0-1 + * @param {string} signal.criticality - CRITICAL, WARNING, INFO + * @param {string} signal.reason - Explication + */ + async processSignal(signal) { + console.log('\n' + '═'.repeat(80)); + console.log(` TRAITEMENT DU SIGNAL`); + console.log(` Action : ${signal.action}`); + console.log(` Paire : ${signal.pair}`); + console.log(` User : ${signal.userId}`); + console.log('═'.repeat(80) + '\n'); + + try { + // ───────────────────────────────────────────── + // ÉTAPE 1 : Récupérer les règles actives + // ───────────────────────────────────────────── + // Utilise le repository injecté (pas d'import hardcodé) + const rules = await alertsRepo.findActiveRulesForSignal( + signal.userId, + signal.pairId + ); + + if (rules.length === 0) { + console.log(' Aucune règle active trouvée pour ce signal'); + console.log(' → Vérifie que des règles sont configurées en DB\n'); + return; + } + + console.log(` ${rules.length} règle(s) trouvée(s)\n`); + + // ───────────────────────────────────────────── + // ÉTAPE 2 : Traiter chaque règle + // ───────────────────────────────────────────── + let sentCount = 0; + + for (const rule of rules) { + console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`); + + // Vérifier si on doit envoyer + if (!this.shouldSendAlert(rule, signal)) { + console.log(`└─ Alerte NON envoyée\n`); + continue; + } + + // Envoyer via le canal + const status = await this.sendViaChannel(rule, signal); + + // Préparer les données de l'événement + const eventData = { + alert_event_id: uuidv4(), + rule_id: rule.rule_id, + timestamp_ms: Date.now(), + severity: signal.criticality, + channel: rule.channel, + send_status: status, + title: `${signal.action} ${signal.pair}`, + message: signal.reason, + signal_action: signal.action, + confidence: signal.confidence, + correlation_id: signal.correlationId || null + }; + + // Sauvegarder dans alert_events (via repo injecté) + await alertsRepo.saveAlertEvent(eventData); + + // Mettre à jour last_notified_at si envoi réussi (via repo injecté) + if (status === 'SENT') { + await alertsRepo.updateLastNotified(rule.rule_id, Date.now()); + sentCount++; + console.log(`└─ Alerte envoyée avec succès\n`); + } else { + console.log(`└─ Échec de l'envoi\n`); + } + } + + // ───────────────────────────────────────────── + // RÉSUMÉ FINAL + // ───────────────────────────────────────────── + console.log('═'.repeat(80)); + console.log(` RÉSUMÉ`); + console.log(` Règles vérifiées : ${rules.length}`); + console.log(` Alertes envoyées : ${sentCount}`); + console.log('═'.repeat(80) + '\n'); + + } catch (error) { + console.error(' ERREUR CRITIQUE dans processSignal:'); + console.error(error); + throw error; // Remonter l'erreur pour que le controller puisse la gérer + } + } + }; +} diff --git a/Wallette/server/modules/alerts/channels/console.js b/Wallette/server/modules/alerts/channels/console.js index c459871..e3077c0 100644 --- a/Wallette/server/modules/alerts/channels/console.js +++ b/Wallette/server/modules/alerts/channels/console.js @@ -1,233 +1,233 @@ -// ========================================================= -// CANAL CONSOLE - Notifications dans la console -// ========================================================= -// RÔLE : Afficher les alertes dans le terminal -// ========================================================= -// UTILITÉ : -// - Développement et debugging -// - Tests rapides sans configurer email/telegram -// - Logs pour surveillance serveur -// ========================================================= - -// ========================================================= -// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT) -// ========================================================= -// Ces codes permettent de colorer le texte dans la console -const COLORS = { - RESET: '\x1b[0m', - - // Couleurs de texte - RED: '\x1b[31m', - GREEN: '\x1b[32m', - YELLOW: '\x1b[33m', - BLUE: '\x1b[34m', - MAGENTA: '\x1b[35m', - CYAN: '\x1b[36m', - WHITE: '\x1b[37m', - - // Styles - BOLD: '\x1b[1m', - DIM: '\x1b[2m', - - // Fond - BG_RED: '\x1b[41m', - BG_YELLOW: '\x1b[43m', - BG_BLUE: '\x1b[44m' -}; - -// ========================================================= -// FONCTION PRINCIPALE : Afficher une alerte dans la console -// ========================================================= -/** - * Affiche une alerte formatée dans la console - * - * @param {Object} signal - Objet contenant les infos du signal - * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS - * @param {string} signal.pair - Paire crypto (ex: BTC/EUR) - * @param {number} signal.confidence - Niveau de confiance (0-1) - * @param {string} signal.reason - Raison de l'alerte - * @param {string} signal.criticality - CRITICAL, WARNING, INFO - * - * @returns {Promise} Toujours 'SENT' (console ne peut pas échouer) - */ -export async function sendConsoleAlert(signal) { - - // ===================================================== - // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ - // ===================================================== - let color; - let bgColor; - let icon; - - switch (signal.criticality) { - case 'CRITICAL': - color = COLORS.RED; - bgColor = COLORS.BG_RED; - icon = '🚨'; - break; - case 'WARNING': - color = COLORS.YELLOW; - bgColor = COLORS.BG_YELLOW; - icon = '⚠️'; - break; - case 'INFO': - default: - color = COLORS.BLUE; - bgColor = COLORS.BG_BLUE; - icon = 'ℹ️'; - break; - } - - // ===================================================== - // 2. FORMATER LA CONFIDENCE EN POURCENTAGE - // ===================================================== - const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2); - - // ===================================================== - // 3. CRÉER LE TIMESTAMP - // ===================================================== - const timestamp = new Date().toLocaleString('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - - // ===================================================== - // 4. AFFICHER L'ALERTE AVEC MISE EN FORME - // ===================================================== - console.log('\n'); // Ligne vide avant - console.log('═'.repeat(80)); // Ligne de séparation - - // En-tête avec couleur de fond - console.log( - `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` + - `${icon} ALERTE ${signal.criticality} `.padEnd(80) + - `${COLORS.RESET}` - ); - - // Ligne de séparation - console.log('─'.repeat(80)); - - // Action recommandée (en couleur et gras) - console.log( - `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` + - `${getActionEmoji(signal.action)} ${signal.action}` - ); - - // Paire crypto - console.log( - `${COLORS.BOLD}${COLORS.CYAN}PAIRE :${COLORS.RESET} ` + - `${signal.pair}` - ); - - // Niveau de confiance avec barre visuelle - const confidenceBar = createProgressBar(signal.confidence); - console.log( - `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` + - `${confidenceBar} ${confidencePercent}%` - ); - - // Ligne de séparation - console.log('─'.repeat(80)); - - // Raison (analyse) - console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`); - console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères - - // Ligne de séparation - console.log('─'.repeat(80)); - - // Timestamp en grisé - console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`); - - // Ligne de fin - console.log('═'.repeat(80)); - console.log('\n'); // Ligne vide après - - // ===================================================== - // 5. RETOURNER LE STATUT - // ===================================================== - // La console ne peut jamais échouer, donc toujours SENT - return 'SENT'; -} - -// ========================================================= -// FONCTIONS UTILITAIRES -// ========================================================= - -/** - * Retourne l'emoji correspondant à l'action - */ -function getActionEmoji(action) { - const emojis = { - 'BUY': '🟢', - 'SELL': '🔴', - 'HOLD': '🟡', - 'STOP_LOSS': '🛑' - }; - return emojis[action] || '📊'; -} - -/** - * Crée une barre de progression visuelle pour la confidence - * Exemple : ████████░░ (80%) - */ -function createProgressBar(confidence, length = 20) { - const filled = Math.round(confidence * length); - const empty = length - filled; - - const bar = - COLORS.GREEN + '█'.repeat(filled) + - COLORS.DIM + '░'.repeat(empty) + - COLORS.RESET; - - return `[${bar}]`; -} - -/** - * Coupe le texte en plusieurs lignes si trop long - * Utile pour que la raison ne dépasse pas la largeur du terminal - */ -function wrapText(text, maxWidth) { - const words = text.split(' '); - const lines = []; - let currentLine = ' '; // Indentation - - words.forEach(word => { - if ((currentLine + word).length > maxWidth) { - lines.push(currentLine); - currentLine = ' ' + word + ' '; - } else { - currentLine += word + ' '; - } - }); - - if (currentLine.trim()) { - lines.push(currentLine); - } - - return lines.join('\n'); -} - -// ========================================================= -// EXPORT D'UNE FONCTION DE TEST -// ========================================================= -/** - * Fonction pour tester rapidement le canal console - * Usage : node test-console.js - */ -export function testConsoleAlert() { - const fakeSignal = { - action: 'BUY', - pair: 'BTC/EUR', - confidence: 0.87, - criticality: 'WARNING', - reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.' - }; - - sendConsoleAlert(fakeSignal); -} - +// ========================================================= +// CANAL CONSOLE - Notifications dans la console +// ========================================================= +// RÔLE : Afficher les alertes dans le terminal +// ========================================================= +// UTILITÉ : +// - Développement et debugging +// - Tests rapides sans configurer email/telegram +// - Logs pour surveillance serveur +// ========================================================= + +// ========================================================= +// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT) +// ========================================================= +// Ces codes permettent de colorer le texte dans la console +const COLORS = { + RESET: '\x1b[0m', + + // Couleurs de texte + RED: '\x1b[31m', + GREEN: '\x1b[32m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + MAGENTA: '\x1b[35m', + CYAN: '\x1b[36m', + WHITE: '\x1b[37m', + + // Styles + BOLD: '\x1b[1m', + DIM: '\x1b[2m', + + // Fond + BG_RED: '\x1b[41m', + BG_YELLOW: '\x1b[43m', + BG_BLUE: '\x1b[44m' +}; + +// ========================================================= +// FONCTION PRINCIPALE : Afficher une alerte dans la console +// ========================================================= +/** + * Affiche une alerte formatée dans la console + * + * @param {Object} signal - Objet contenant les infos du signal + * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS + * @param {string} signal.pair - Paire crypto (ex: BTC/EUR) + * @param {number} signal.confidence - Niveau de confiance (0-1) + * @param {string} signal.reason - Raison de l'alerte + * @param {string} signal.criticality - CRITICAL, WARNING, INFO + * + * @returns {Promise} Toujours 'SENT' (console ne peut pas échouer) + */ +export async function sendConsoleAlert(signal) { + + // ===================================================== + // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ + // ===================================================== + let color; + let bgColor; + let icon; + + switch (signal.criticality) { + case 'CRITICAL': + color = COLORS.RED; + bgColor = COLORS.BG_RED; + icon = '🚨'; + break; + case 'WARNING': + color = COLORS.YELLOW; + bgColor = COLORS.BG_YELLOW; + icon = '⚠️'; + break; + case 'INFO': + default: + color = COLORS.BLUE; + bgColor = COLORS.BG_BLUE; + icon = 'ℹ️'; + break; + } + + // ===================================================== + // 2. FORMATER LA CONFIDENCE EN POURCENTAGE + // ===================================================== + const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2); + + // ===================================================== + // 3. CRÉER LE TIMESTAMP + // ===================================================== + const timestamp = new Date().toLocaleString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + // ===================================================== + // 4. AFFICHER L'ALERTE AVEC MISE EN FORME + // ===================================================== + console.log('\n'); // Ligne vide avant + console.log('═'.repeat(80)); // Ligne de séparation + + // En-tête avec couleur de fond + console.log( + `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` + + `${icon} ALERTE ${signal.criticality} `.padEnd(80) + + `${COLORS.RESET}` + ); + + // Ligne de séparation + console.log('─'.repeat(80)); + + // Action recommandée (en couleur et gras) + console.log( + `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` + + `${getActionEmoji(signal.action)} ${signal.action}` + ); + + // Paire crypto + console.log( + `${COLORS.BOLD}${COLORS.CYAN}PAIRE :${COLORS.RESET} ` + + `${signal.pair}` + ); + + // Niveau de confiance avec barre visuelle + const confidenceBar = createProgressBar(signal.confidence); + console.log( + `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` + + `${confidenceBar} ${confidencePercent}%` + ); + + // Ligne de séparation + console.log('─'.repeat(80)); + + // Raison (analyse) + console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`); + console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères + + // Ligne de séparation + console.log('─'.repeat(80)); + + // Timestamp en grisé + console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`); + + // Ligne de fin + console.log('═'.repeat(80)); + console.log('\n'); // Ligne vide après + + // ===================================================== + // 5. RETOURNER LE STATUT + // ===================================================== + // La console ne peut jamais échouer, donc toujours SENT + return 'SENT'; +} + +// ========================================================= +// FONCTIONS UTILITAIRES +// ========================================================= + +/** + * Retourne l'emoji correspondant à l'action + */ +function getActionEmoji(action) { + const emojis = { + 'BUY': '🟢', + 'SELL': '🔴', + 'HOLD': '🟡', + 'STOP_LOSS': '🛑' + }; + return emojis[action] || '📊'; +} + +/** + * Crée une barre de progression visuelle pour la confidence + * Exemple : ████████░░ (80%) + */ +function createProgressBar(confidence, length = 20) { + const filled = Math.round(confidence * length); + const empty = length - filled; + + const bar = + COLORS.GREEN + '█'.repeat(filled) + + COLORS.DIM + '░'.repeat(empty) + + COLORS.RESET; + + return `[${bar}]`; +} + +/** + * Coupe le texte en plusieurs lignes si trop long + * Utile pour que la raison ne dépasse pas la largeur du terminal + */ +function wrapText(text, maxWidth) { + const words = text.split(' '); + const lines = []; + let currentLine = ' '; // Indentation + + words.forEach(word => { + if ((currentLine + word).length > maxWidth) { + lines.push(currentLine); + currentLine = ' ' + word + ' '; + } else { + currentLine += word + ' '; + } + }); + + if (currentLine.trim()) { + lines.push(currentLine); + } + + return lines.join('\n'); +} + +// ========================================================= +// EXPORT D'UNE FONCTION DE TEST +// ========================================================= +/** + * Fonction pour tester rapidement le canal console + * Usage : node test-console.js + */ +export function testConsoleAlert() { + const fakeSignal = { + action: 'BUY', + pair: 'BTC/EUR', + confidence: 0.87, + criticality: 'WARNING', + reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.' + }; + + sendConsoleAlert(fakeSignal); +} + diff --git a/Wallette/server/modules/alerts/channels/discord.js b/Wallette/server/modules/alerts/channels/discord.js index f3491d8..a2c9ce8 100644 --- a/Wallette/server/modules/alerts/channels/discord.js +++ b/Wallette/server/modules/alerts/channels/discord.js @@ -1,23 +1,23 @@ -// !!! A FAIRE !!! - -// ========================================================= -// CANAL DISCORD - Notifications via Discord Webhook -// ========================================================= -// RÔLE : Envoyer des alertes dans un channel Discord -// ========================================================= - -/** - * Envoie une alerte via Discord Webhook - * - * @param {string} webhookUrl - URL du webhook Discord - * @param {Object} signal - Signal crypto - * @returns {Promise} 'SENT' ou 'FAILED' - */ -export async function sendDiscordAlert(webhookUrl, signal) { - // TODO: À implémenter si nécessaire - - console.log('💬 Canal Discord pas encore implémenté'); - - return 'FAILED'; - -} +// !!! A FAIRE !!! + +// ========================================================= +// CANAL DISCORD - Notifications via Discord Webhook +// ========================================================= +// RÔLE : Envoyer des alertes dans un channel Discord +// ========================================================= + +/** + * Envoie une alerte via Discord Webhook + * + * @param {string} webhookUrl - URL du webhook Discord + * @param {Object} signal - Signal crypto + * @returns {Promise} 'SENT' ou 'FAILED' + */ +export async function sendDiscordAlert(webhookUrl, signal) { + // TODO: À implémenter si nécessaire + + console.log('💬 Canal Discord pas encore implémenté'); + + return 'FAILED'; + +} diff --git a/Wallette/server/modules/alerts/channels/mailer.js b/Wallette/server/modules/alerts/channels/mailer.js index 1a4ebeb..9512da6 100644 --- a/Wallette/server/modules/alerts/channels/mailer.js +++ b/Wallette/server/modules/alerts/channels/mailer.js @@ -1,226 +1,226 @@ -// ========================================================= -// CANAL EMAIL - Notifications par email -// ========================================================= -// RÔLE : Envoyer des alertes par email avec Nodemailer -// ========================================================= - -import nodemailer from 'nodemailer'; - -// ========================================================= -// FONCTION PRINCIPALE : Envoyer une alerte par email -// ========================================================= -/** - * Envoie une alerte par email - * - * @param {string} to - Adresse email du destinataire - * @param {Object} signal - Objet contenant les infos du signal - * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS - * @param {string} signal.pair - Paire crypto (ex: BTC/EUR) - * @param {number} signal.confidence - Niveau de confiance (0-1) - * @param {string} signal.reason - Raison de l'alerte - * @param {string} signal.criticality - CRITICAL, WARNING, INFO - * - * @returns {Promise} 'SENT' ou 'FAILED' - */ -export async function sendAlertEmail(to, signal) { - - // ===================================================== - // 1. CRÉER LE TRANSPORTEUR SMTP - // ===================================================== - // Le transporteur est l'objet qui envoie les emails - // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.) - const transporter = nodemailer.createTransport({ - host: process.env.MAIL_HOST, // ex: smtp.gmail.com - port: parseInt(process.env.MAIL_PORT), // ex: 587 - secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres - auth: { - user: process.env.MAIL_USER, // Ton adresse email - pass: process.env.MAIL_PASS // Ton mot de passe ou App Password - } - }); - - try { - // ===================================================== - // 2. FORMATER LES DONNÉES - // ===================================================== - // Convertir la confidence en pourcentage - const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2); - - // Choisir l'emoji selon l'action - const emoji = getEmojiForAction(signal.action); - - // Choisir la couleur selon la criticité - const color = getColorForCriticality(signal.criticality); - - // ===================================================== - // 3. CRÉER LE CONTENU DE L'EMAIL - // ===================================================== - const mailOptions = { - from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`, - to: to, - subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`, - - // Version texte (pour clients email sans HTML) - text: ` -🚨 ALERTE CRYPTO 🚨 - -Action recommandée : ${signal.action} -Paire : ${signal.pair} -Confiance : ${confidencePercent}% -Criticité : ${signal.criticality} - -Raison : -${signal.reason} - ---- -Wall-e-tte - Votre conseiller crypto - `.trim(), - - // Version HTML (plus jolie) - html: ` - - - - - - -
-
-

${emoji} ALERTE ${signal.criticality}

-
-
-

Recommandation : ${signal.action}

-

Paire : ${signal.pair}

- -
-
Confiance
-
${confidencePercent}%
-
- -
-

Analyse :

-

${signal.reason}

-
- -

- Timestamp : ${new Date().toLocaleString('fr-FR')} -

-
- -
- - - ` - }; - - // ===================================================== - // 4. ENVOYER L'EMAIL - // ===================================================== - const info = await transporter.sendMail(mailOptions); - - console.log(`Email envoyé avec succès !`); - console.log(` → Destinataire : ${to}`); - console.log(` → Message ID : ${info.messageId}`); - - return 'SENT'; - - } catch (error) { - // ===================================================== - // 5. GESTION DES ERREURS - // ===================================================== - console.error('Erreur lors de l\'envoi de l\'email :'); - console.error(` → ${error.message}`); - - // Erreurs communes et solutions - if (error.message.includes('authentication')) { - console.error(' Vérifie MAIL_USER et MAIL_PASS dans .env'); - } else if (error.message.includes('ECONNREFUSED')) { - console.error(' Vérifie MAIL_HOST et MAIL_PORT dans .env'); - } - - return 'FAILED'; - } -} - -// ========================================================= -// FONCTIONS UTILITAIRES -// ========================================================= - -/** - * Retourne un emoji selon l'action recommandée - */ -function getEmojiForAction(action) { - const emojis = { - 'BUY': '🟢', - 'SELL': '🔴', - 'HOLD': '🟡', - 'STOP_LOSS': '🛑' - }; - return emojis[action] || '📊'; -} - -/** - * Retourne une couleur selon le niveau de criticité - */ -function getColorForCriticality(criticality) { - const colors = { - 'CRITICAL': '#dc3545', // Rouge - 'WARNING': '#ffc107', // Orange/Jaune - 'INFO': '#17a2b8' // Bleu - }; - return colors[criticality] || '#6c757d'; -} - +// ========================================================= +// CANAL EMAIL - Notifications par email +// ========================================================= +// RÔLE : Envoyer des alertes par email avec Nodemailer +// ========================================================= + +import nodemailer from 'nodemailer'; + +// ========================================================= +// FONCTION PRINCIPALE : Envoyer une alerte par email +// ========================================================= +/** + * Envoie une alerte par email + * + * @param {string} to - Adresse email du destinataire + * @param {Object} signal - Objet contenant les infos du signal + * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS + * @param {string} signal.pair - Paire crypto (ex: BTC/EUR) + * @param {number} signal.confidence - Niveau de confiance (0-1) + * @param {string} signal.reason - Raison de l'alerte + * @param {string} signal.criticality - CRITICAL, WARNING, INFO + * + * @returns {Promise} 'SENT' ou 'FAILED' + */ +export async function sendAlertEmail(to, signal) { + + // ===================================================== + // 1. CRÉER LE TRANSPORTEUR SMTP + // ===================================================== + // Le transporteur est l'objet qui envoie les emails + // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.) + const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, // ex: smtp.gmail.com + port: parseInt(process.env.MAIL_PORT), // ex: 587 + secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres + auth: { + user: process.env.MAIL_USER, // Ton adresse email + pass: process.env.MAIL_PASS // Ton mot de passe ou App Password + } + }); + + try { + // ===================================================== + // 2. FORMATER LES DONNÉES + // ===================================================== + // Convertir la confidence en pourcentage + const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2); + + // Choisir l'emoji selon l'action + const emoji = getEmojiForAction(signal.action); + + // Choisir la couleur selon la criticité + const color = getColorForCriticality(signal.criticality); + + // ===================================================== + // 3. CRÉER LE CONTENU DE L'EMAIL + // ===================================================== + const mailOptions = { + from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`, + to: to, + subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`, + + // Version texte (pour clients email sans HTML) + text: ` +🚨 ALERTE CRYPTO 🚨 + +Action recommandée : ${signal.action} +Paire : ${signal.pair} +Confiance : ${confidencePercent}% +Criticité : ${signal.criticality} + +Raison : +${signal.reason} + +--- +Wall-e-tte - Votre conseiller crypto + `.trim(), + + // Version HTML (plus jolie) + html: ` + + + + + + +
+
+

${emoji} ALERTE ${signal.criticality}

+
+
+

Recommandation : ${signal.action}

+

Paire : ${signal.pair}

+ +
+
Confiance
+
${confidencePercent}%
+
+ +
+

Analyse :

+

${signal.reason}

+
+ +

+ Timestamp : ${new Date().toLocaleString('fr-FR')} +

+
+ +
+ + + ` + }; + + // ===================================================== + // 4. ENVOYER L'EMAIL + // ===================================================== + const info = await transporter.sendMail(mailOptions); + + console.log(`Email envoyé avec succès !`); + console.log(` → Destinataire : ${to}`); + console.log(` → Message ID : ${info.messageId}`); + + return 'SENT'; + + } catch (error) { + // ===================================================== + // 5. GESTION DES ERREURS + // ===================================================== + console.error('Erreur lors de l\'envoi de l\'email :'); + console.error(` → ${error.message}`); + + // Erreurs communes et solutions + if (error.message.includes('authentication')) { + console.error(' Vérifie MAIL_USER et MAIL_PASS dans .env'); + } else if (error.message.includes('ECONNREFUSED')) { + console.error(' Vérifie MAIL_HOST et MAIL_PORT dans .env'); + } + + return 'FAILED'; + } +} + +// ========================================================= +// FONCTIONS UTILITAIRES +// ========================================================= + +/** + * Retourne un emoji selon l'action recommandée + */ +function getEmojiForAction(action) { + const emojis = { + 'BUY': '🟢', + 'SELL': '🔴', + 'HOLD': '🟡', + 'STOP_LOSS': '🛑' + }; + return emojis[action] || '📊'; +} + +/** + * Retourne une couleur selon le niveau de criticité + */ +function getColorForCriticality(criticality) { + const colors = { + 'CRITICAL': '#dc3545', // Rouge + 'WARNING': '#ffc107', // Orange/Jaune + 'INFO': '#17a2b8' // Bleu + }; + return colors[criticality] || '#6c757d'; +} + diff --git a/Wallette/server/modules/alerts/channels/telegram.js b/Wallette/server/modules/alerts/channels/telegram.js index ce3a74e..aa50b3e 100644 --- a/Wallette/server/modules/alerts/channels/telegram.js +++ b/Wallette/server/modules/alerts/channels/telegram.js @@ -1,55 +1,55 @@ -// !!! À FAIRE !!! - - -// ========================================================= -// CANAL TELEGRAM - Notifications via Telegram Bot -// ========================================================= -// RÔLE : Envoyer des alertes via un bot Telegram -// ========================================================= - -// ========================================================= -// TODO : CONFIGURATION -// ========================================================= -/* - Pour utiliser Telegram, il faut : - - 1. Créer un bot Telegram : - - Parler à @BotFather sur Telegram - - Utiliser /newbot - - Récupérer le TOKEN - - 2. Récupérer ton CHAT_ID : - - Parler à ton bot - - Aller sur : https://api.telegram.org/bot/getUpdates - - Récupérer ton chat_id dans le JSON - - 3. Ajouter dans .env : - TELEGRAM_BOT_TOKEN=123456:ABC-DEF... - TELEGRAM_CHAT_ID=123456789 - - 4. Tester l'envoi : - - Utiliser fetch() ou axios - - POST vers https://api.telegram.org/bot/sendMessage -*/ - -// ========================================================= -// FONCTION À IMPLÉMENTER -// ========================================================= -/** - * Envoie une alerte via Telegram - * - * @param {string} chatId - ID du chat Telegram - * @param {Object} signal - Signal crypto - * @returns {Promise} 'SENT' ou 'FAILED' - */ -export async function sendTelegramAlert(chatId, signal) { - // TODO: À implémenter ensemble - - console.log('📱 Canal Telegram pas encore implémenté'); - console.log(` → chatId: ${chatId}`); - console.log(` → signal: ${signal.action} sur ${signal.pair}`); - - // Pour l'instant, on retourne FAILED - return 'FAILED'; -} - +// !!! À FAIRE !!! + + +// ========================================================= +// CANAL TELEGRAM - Notifications via Telegram Bot +// ========================================================= +// RÔLE : Envoyer des alertes via un bot Telegram +// ========================================================= + +// ========================================================= +// TODO : CONFIGURATION +// ========================================================= +/* + Pour utiliser Telegram, il faut : + + 1. Créer un bot Telegram : + - Parler à @BotFather sur Telegram + - Utiliser /newbot + - Récupérer le TOKEN + + 2. Récupérer ton CHAT_ID : + - Parler à ton bot + - Aller sur : https://api.telegram.org/bot/getUpdates + - Récupérer ton chat_id dans le JSON + + 3. Ajouter dans .env : + TELEGRAM_BOT_TOKEN=123456:ABC-DEF... + TELEGRAM_CHAT_ID=123456789 + + 4. Tester l'envoi : + - Utiliser fetch() ou axios + - POST vers https://api.telegram.org/bot/sendMessage +*/ + +// ========================================================= +// FONCTION À IMPLÉMENTER +// ========================================================= +/** + * Envoie une alerte via Telegram + * + * @param {string} chatId - ID du chat Telegram + * @param {Object} signal - Signal crypto + * @returns {Promise} 'SENT' ou 'FAILED' + */ +export async function sendTelegramAlert(chatId, signal) { + // TODO: À implémenter ensemble + + console.log('📱 Canal Telegram pas encore implémenté'); + console.log(` → chatId: ${chatId}`); + console.log(` → signal: ${signal.action} sur ${signal.pair}`); + + // Pour l'instant, on retourne FAILED + return 'FAILED'; +} + diff --git a/Wallette/server/modules/alerts/channels/web.js b/Wallette/server/modules/alerts/channels/web.js index d70c149..a882a09 100644 --- a/Wallette/server/modules/alerts/channels/web.js +++ b/Wallette/server/modules/alerts/channels/web.js @@ -1,76 +1,76 @@ -// ========================================================= -// CANAL WEB - Notifications temps réel via Socket.IO -// ========================================================= -// RÔLE : Envoyer des alertes en temps réel aux clients -// web (Océane) et mobile (Thibaud) -// ========================================================= -// DÉPENDANCE : socketManager.js doit être initialisé -// ========================================================= - -import { sendAlertToUser, isUserConnected } from '../socketManager.js'; - -/** - * Envoie une alerte via WebSocket à un utilisateur - * - * @param {string} userId - ID de l'utilisateur - * @param {Object} signal - Signal crypto à envoyer - * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD' - * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR') - * @param {number} signal.confidence - Niveau de confiance (0-1) - * @param {string} signal.reason - Raison du signal - * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL' - * @returns {Promise} 'SENT' ou 'FAILED' - * - * @example - * const status = await sendWebAlert('user-123', { - * action: 'BUY', - * pair: 'BTC/EUR', - * confidence: 0.85, - * reason: 'RSI indique une survente', - * criticality: 'WARNING' - * }); - */ -export async function sendWebAlert(userId, signal) { - // Vérifier si l'utilisateur est connecté - if (!isUserConnected(userId)) { - console.log(`⚠️ [Web Channel] User ${userId} non connecté - alerte non envoyée`); - return 'FAILED'; - } - - // Formater l'alerte pour les clients - const alertPayload = { - // Données du signal - action: signal.action, - pair: signal.pair, - confidence: signal.confidence, - reason: signal.reason, - - // Métadonnées - alertLevel: signal.criticality || 'INFO', - timestamp: Date.now(), - - // Prix si disponible - ...(signal.priceAtSignal && { price: signal.priceAtSignal }) - }; - - // Envoyer via Socket.IO - const sent = sendAlertToUser(userId, alertPayload); - - if (sent) { - console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`); - return 'SENT'; - } - - return 'FAILED'; -} - -/** - * Vérifie si le canal web est disponible pour un utilisateur - * (utile pour le service avant de tenter l'envoi) - * - * @param {string} userId - ID de l'utilisateur - * @returns {boolean} true si l'utilisateur peut recevoir des alertes web - */ -export function isWebChannelAvailable(userId) { - return isUserConnected(userId); -} +// ========================================================= +// CANAL WEB - Notifications temps réel via Socket.IO +// ========================================================= +// RÔLE : Envoyer des alertes en temps réel aux clients +// web (Océane) et mobile (Thibaud) +// ========================================================= +// DÉPENDANCE : socketManager.js doit être initialisé +// ========================================================= + +import { sendAlertToUser, isUserConnected } from '../socketManager.js'; + +/** + * Envoie une alerte via WebSocket à un utilisateur + * + * @param {string} userId - ID de l'utilisateur + * @param {Object} signal - Signal crypto à envoyer + * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD' + * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR') + * @param {number} signal.confidence - Niveau de confiance (0-1) + * @param {string} signal.reason - Raison du signal + * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL' + * @returns {Promise} 'SENT' ou 'FAILED' + * + * @example + * const status = await sendWebAlert('user-123', { + * action: 'BUY', + * pair: 'BTC/EUR', + * confidence: 0.85, + * reason: 'RSI indique une survente', + * criticality: 'WARNING' + * }); + */ +export async function sendWebAlert(userId, signal) { + // Vérifier si l'utilisateur est connecté + if (!isUserConnected(userId)) { + console.log(`⚠️ [Web Channel] User ${userId} non connecté - alerte non envoyée`); + return 'FAILED'; + } + + // Formater l'alerte pour les clients + const alertPayload = { + // Données du signal + action: signal.action, + pair: signal.pair, + confidence: signal.confidence, + reason: signal.reason, + + // Métadonnées + alertLevel: signal.criticality || 'INFO', + timestamp: Date.now(), + + // Prix si disponible + ...(signal.priceAtSignal && { price: signal.priceAtSignal }) + }; + + // Envoyer via Socket.IO + const sent = sendAlertToUser(userId, alertPayload); + + if (sent) { + console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`); + return 'SENT'; + } + + return 'FAILED'; +} + +/** + * Vérifie si le canal web est disponible pour un utilisateur + * (utile pour le service avant de tenter l'envoi) + * + * @param {string} userId - ID de l'utilisateur + * @returns {boolean} true si l'utilisateur peut recevoir des alertes web + */ +export function isWebChannelAvailable(userId) { + return isUserConnected(userId); +} diff --git a/Wallette/server/modules/alerts/index.js b/Wallette/server/modules/alerts/index.js index 18f74ca..776c227 100644 --- a/Wallette/server/modules/alerts/index.js +++ b/Wallette/server/modules/alerts/index.js @@ -1,76 +1,76 @@ -// ========================================================= -// MODULE ALERTS - Point d'entrée -// ========================================================= - -// Export des factory functions principales -export { createAlertsRepo } from './alerts.repo.js'; -export { createAlertsService } from './alerts.service.js'; -export { createAlertsController } from './alerts.controller.js'; -export { createAlertsRouter } from './alerts.router.js'; - -// Export des adapters (pour différentes bases de données) -export { createMySQLAdapter } from './adapters/mysql.adapter.js'; - -// Export du gestionnaire Socket.IO -export { - initSocketIO, - sendAlertToUser, - broadcastAlert, - isUserConnected, - getConnectedUsersCount, - getIO -} from './socketManager.js'; - -// Export des canaux (utiles si on veut les tester séparément) -export { sendAlertEmail } from './channels/mailer.js'; -export { sendConsoleAlert } from './channels/console.js'; -export { sendTelegramAlert } from './channels/telegram.js'; -export { sendDiscordAlert } from './channels/discord.js'; -export { sendWebAlert } from './channels/web.js'; - -// ========================================================= -// EXEMPLE D'UTILISATION RAPIDE -// ========================================================= -/* - DANS WALL-E-TTE : - - // 1. Import du module - import db from './config/db.js'; - import { - createAlertsRepo, - createAlertsService, - createAlertsController, - createAlertsRouter - } from './modules/alerts/index.js'; - - // 2. Initialiser le module (dans l'ordre !) - const alertsRepo = createAlertsRepo(db); - const alertsService = createAlertsService(alertsRepo); - const alertsController = createAlertsController(alertsService, alertsRepo); - const alertsRouter = createAlertsRouter(alertsController); - - // 3. Utiliser dans Express - app.use('/api/alerts', alertsRouter); - - // 4. Ou utiliser directement le service - await alertsService.processSignal(mySignal); - - ═══════════════════════════════════════════════════════ - - DANS UN AUTRE PROJET : - - // Exactement pareil ! - import myDb from './my-db-config.js'; - import { - createAlertsRepo, - createAlertsService - } from '@wall-e-tte/alerts-module'; - - const repo = createAlertsRepo(myDb, { - alertsTable: 'my_notifications', - usersTable: 'my_users' - }); - const service = createAlertsService(repo); - - await service.processSignal(signal); // Ça marche ! -*/ +// ========================================================= +// MODULE ALERTS - Point d'entrée +// ========================================================= + +// Export des factory functions principales +export { createAlertsRepo } from './alerts.repo.js'; +export { createAlertsService } from './alerts.service.js'; +export { createAlertsController } from './alerts.controller.js'; +export { createAlertsRouter } from './alerts.router.js'; + +// Export des adapters (pour différentes bases de données) +export { createMySQLAdapter } from './adapters/mysql.adapter.js'; + +// Export du gestionnaire Socket.IO +export { + initSocketIO, + sendAlertToUser, + broadcastAlert, + isUserConnected, + getConnectedUsersCount, + getIO +} from './socketManager.js'; + +// Export des canaux (utiles si on veut les tester séparément) +export { sendAlertEmail } from './channels/mailer.js'; +export { sendConsoleAlert } from './channels/console.js'; +export { sendTelegramAlert } from './channels/telegram.js'; +export { sendDiscordAlert } from './channels/discord.js'; +export { sendWebAlert } from './channels/web.js'; + +// ========================================================= +// EXEMPLE D'UTILISATION RAPIDE +// ========================================================= +/* + DANS WALL-E-TTE : + + // 1. Import du module + import db from './config/db.js'; + import { + createAlertsRepo, + createAlertsService, + createAlertsController, + createAlertsRouter + } from './modules/alerts/index.js'; + + // 2. Initialiser le module (dans l'ordre !) + const alertsRepo = createAlertsRepo(db); + const alertsService = createAlertsService(alertsRepo); + const alertsController = createAlertsController(alertsService, alertsRepo); + const alertsRouter = createAlertsRouter(alertsController); + + // 3. Utiliser dans Express + app.use('/api/alerts', alertsRouter); + + // 4. Ou utiliser directement le service + await alertsService.processSignal(mySignal); + + ═══════════════════════════════════════════════════════ + + DANS UN AUTRE PROJET : + + // Exactement pareil ! + import myDb from './my-db-config.js'; + import { + createAlertsRepo, + createAlertsService + } from '@wall-e-tte/alerts-module'; + + const repo = createAlertsRepo(myDb, { + alertsTable: 'my_notifications', + usersTable: 'my_users' + }); + const service = createAlertsService(repo); + + await service.processSignal(signal); // Ça marche ! +*/ diff --git a/Wallette/server/modules/alerts/socketManager.js b/Wallette/server/modules/alerts/socketManager.js index ef6ff14..931ed75 100644 --- a/Wallette/server/modules/alerts/socketManager.js +++ b/Wallette/server/modules/alerts/socketManager.js @@ -1,194 +1,194 @@ -// ========================================================= -// SOCKET MANAGER - Gestion des connexions WebSocket -// ========================================================= -// RÔLE : Centraliser la gestion des connexions Socket.IO -// pour le module d'alertes -// ========================================================= -// UTILISÉ PAR : web.js -// ========================================================= - -import { Server } from 'socket.io'; - -// ========================================================= -// STOCKAGE DES CONNEXIONS -// ========================================================= -// Map : userId → socket -// Permet d'envoyer une alerte à un utilisateur spécifique -const userSockets = new Map(); - -// Instance du serveur Socket.IO -let io = null; - -// ========================================================= -// INITIALISATION DU SERVEUR SOCKET.IO -// ========================================================= -/** - * Initialise Socket.IO sur un serveur HTTP existant - * - * @param {Object} httpServer - Serveur HTTP (Express) - * @param {Object} options - Options Socket.IO (optionnel) - * @returns {Server} Instance Socket.IO - * - * @example - * // Dans le fichier principal du serveur (index.js) - * import express from 'express'; - * import { createServer } from 'http'; - * import { initSocketIO } from './modules/alerts/socketManager.js'; - * - * const app = express(); - * const httpServer = createServer(app); - * const io = initSocketIO(httpServer); - * - * httpServer.listen(3000); - */ -export function initSocketIO(httpServer, options = {}) { - // Configuration par défaut + options personnalisées - const defaultOptions = { - cors: { - origin: "*", // En prod, restreindre aux domaines autorisés - methods: ["GET", "POST"] - } - }; - - io = new Server(httpServer, { ...defaultOptions, ...options }); - - // ───────────────────────────────────────────────────── - // GESTION DES CONNEXIONS - // ───────────────────────────────────────────────────── - io.on('connection', (socket) => { - console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`); - - // ───────────────────────────────────────────────── - // EVENT : Authentification de l'utilisateur - // ───────────────────────────────────────────────── - // Le client doit envoyer son userId après connexion - socket.on('auth', (userId) => { - if (!userId) { - console.log(` [Socket.IO] Auth sans userId (socket: ${socket.id})`); - return; - } - - // Stocker la correspondance userId → socket - userSockets.set(userId, socket); - socket.userId = userId; // Garder une référence sur le socket - - console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`); - console.log(` Utilisateurs connectés : ${userSockets.size}`); - - // Confirmer l'authentification au client - socket.emit('auth_success', { - userId, - message: 'Connecté au système d\'alertes' - }); - }); - - // ───────────────────────────────────────────────── - // EVENT : Déconnexion - // ───────────────────────────────────────────────── - socket.on('disconnect', (reason) => { - if (socket.userId) { - userSockets.delete(socket.userId); - console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`); - console.log(` Utilisateurs connectés : ${userSockets.size}`); - } - }); - - // ───────────────────────────────────────────────── - // EVENT : Ping (test de connexion) - // ───────────────────────────────────────────────── - socket.on('ping_alerts', () => { - socket.emit('pong_alerts', { - timestamp: Date.now(), - status: 'ok' - }); - }); - }); - - console.log(' [Socket.IO] Serveur initialisé'); - return io; -} - -// ========================================================= -// FONCTIONS D'ENVOI D'ALERTES -// ========================================================= - -/** - * Envoie une alerte à un utilisateur spécifique - * - * @param {string} userId - ID de l'utilisateur cible - * @param {Object} alertData - Données de l'alerte - * @returns {boolean} true si envoyé, false si utilisateur non connecté - */ -export function sendAlertToUser(userId, alertData) { - const socket = userSockets.get(userId); - - if (socket) { - socket.emit('alert', { - ...alertData, - timestamp: Date.now() - }); - console.log(` [Socket.IO] Alerte envoyée à ${userId}`); - return true; - } - - console.log(` [Socket.IO] Utilisateur ${userId} non connecté`); - return false; -} - -/** - * Envoie une alerte à tous les utilisateurs connectés - * - * @param {Object} alertData - Données de l'alerte - * @returns {number} Nombre d'utilisateurs notifiés - */ -export function broadcastAlert(alertData) { - if (!io) { - console.error(' [Socket.IO] Serveur non initialisé'); - return 0; - } - - io.emit('alert', { - ...alertData, - timestamp: Date.now() - }); - - console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`); - return userSockets.size; -} - -// ========================================================= -// FONCTIONS UTILITAIRES -// ========================================================= - -/** - * Vérifie si un utilisateur est connecté - * @param {string} userId - * @returns {boolean} - */ -export function isUserConnected(userId) { - return userSockets.has(userId); -} - -/** - * Retourne le nombre d'utilisateurs connectés - * @returns {number} - */ -export function getConnectedUsersCount() { - return userSockets.size; -} - -/** - * Retourne la liste des userIds connectés - * @returns {string[]} - */ -export function getConnectedUserIds() { - return Array.from(userSockets.keys()); -} - -/** - * Retourne l'instance Socket.IO (pour usage avancé) - * @returns {Server|null} - */ -export function getIO() { - return io; -} +// ========================================================= +// SOCKET MANAGER - Gestion des connexions WebSocket +// ========================================================= +// RÔLE : Centraliser la gestion des connexions Socket.IO +// pour le module d'alertes +// ========================================================= +// UTILISÉ PAR : web.js +// ========================================================= + +import { Server } from 'socket.io'; + +// ========================================================= +// STOCKAGE DES CONNEXIONS +// ========================================================= +// Map : userId → socket +// Permet d'envoyer une alerte à un utilisateur spécifique +const userSockets = new Map(); + +// Instance du serveur Socket.IO +let io = null; + +// ========================================================= +// INITIALISATION DU SERVEUR SOCKET.IO +// ========================================================= +/** + * Initialise Socket.IO sur un serveur HTTP existant + * + * @param {Object} httpServer - Serveur HTTP (Express) + * @param {Object} options - Options Socket.IO (optionnel) + * @returns {Server} Instance Socket.IO + * + * @example + * // Dans le fichier principal du serveur (index.js) + * import express from 'express'; + * import { createServer } from 'http'; + * import { initSocketIO } from './modules/alerts/socketManager.js'; + * + * const app = express(); + * const httpServer = createServer(app); + * const io = initSocketIO(httpServer); + * + * httpServer.listen(3000); + */ +export function initSocketIO(httpServer, options = {}) { + // Configuration par défaut + options personnalisées + const defaultOptions = { + cors: { + origin: "*", // En prod, restreindre aux domaines autorisés + methods: ["GET", "POST"] + } + }; + + io = new Server(httpServer, { ...defaultOptions, ...options }); + + // ───────────────────────────────────────────────────── + // GESTION DES CONNEXIONS + // ───────────────────────────────────────────────────── + io.on('connection', (socket) => { + console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`); + + // ───────────────────────────────────────────────── + // EVENT : Authentification de l'utilisateur + // ───────────────────────────────────────────────── + // Le client doit envoyer son userId après connexion + socket.on('auth', (userId) => { + if (!userId) { + console.log(` [Socket.IO] Auth sans userId (socket: ${socket.id})`); + return; + } + + // Stocker la correspondance userId → socket + userSockets.set(userId, socket); + socket.userId = userId; // Garder une référence sur le socket + + console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`); + console.log(` Utilisateurs connectés : ${userSockets.size}`); + + // Confirmer l'authentification au client + socket.emit('auth_success', { + userId, + message: 'Connecté au système d\'alertes' + }); + }); + + // ───────────────────────────────────────────────── + // EVENT : Déconnexion + // ───────────────────────────────────────────────── + socket.on('disconnect', (reason) => { + if (socket.userId) { + userSockets.delete(socket.userId); + console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`); + console.log(` Utilisateurs connectés : ${userSockets.size}`); + } + }); + + // ───────────────────────────────────────────────── + // EVENT : Ping (test de connexion) + // ───────────────────────────────────────────────── + socket.on('ping_alerts', () => { + socket.emit('pong_alerts', { + timestamp: Date.now(), + status: 'ok' + }); + }); + }); + + console.log(' [Socket.IO] Serveur initialisé'); + return io; +} + +// ========================================================= +// FONCTIONS D'ENVOI D'ALERTES +// ========================================================= + +/** + * Envoie une alerte à un utilisateur spécifique + * + * @param {string} userId - ID de l'utilisateur cible + * @param {Object} alertData - Données de l'alerte + * @returns {boolean} true si envoyé, false si utilisateur non connecté + */ +export function sendAlertToUser(userId, alertData) { + const socket = userSockets.get(userId); + + if (socket) { + socket.emit('alert', { + ...alertData, + timestamp: Date.now() + }); + console.log(` [Socket.IO] Alerte envoyée à ${userId}`); + return true; + } + + console.log(` [Socket.IO] Utilisateur ${userId} non connecté`); + return false; +} + +/** + * Envoie une alerte à tous les utilisateurs connectés + * + * @param {Object} alertData - Données de l'alerte + * @returns {number} Nombre d'utilisateurs notifiés + */ +export function broadcastAlert(alertData) { + if (!io) { + console.error(' [Socket.IO] Serveur non initialisé'); + return 0; + } + + io.emit('alert', { + ...alertData, + timestamp: Date.now() + }); + + console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`); + return userSockets.size; +} + +// ========================================================= +// FONCTIONS UTILITAIRES +// ========================================================= + +/** + * Vérifie si un utilisateur est connecté + * @param {string} userId + * @returns {boolean} + */ +export function isUserConnected(userId) { + return userSockets.has(userId); +} + +/** + * Retourne le nombre d'utilisateurs connectés + * @returns {number} + */ +export function getConnectedUsersCount() { + return userSockets.size; +} + +/** + * Retourne la liste des userIds connectés + * @returns {string[]} + */ +export function getConnectedUserIds() { + return Array.from(userSockets.keys()); +} + +/** + * Retourne l'instance Socket.IO (pour usage avancé) + * @returns {Server|null} + */ +export function getIO() { + return io; +} diff --git a/Wallette/server/modules/alerts/test-alerts.js b/Wallette/server/modules/alerts/test-alerts.js index 0ad76cc..220af6f 100644 --- a/Wallette/server/modules/alerts/test-alerts.js +++ b/Wallette/server/modules/alerts/test-alerts.js @@ -1,164 +1,164 @@ -// ========================================================= -// SCRIPT DE TEST - Module Alerts RÉUTILISABLE -// ========================================================= -// Ce script teste ton module d'alertes avec la nouvelle -// architecture réutilisable (injection de dépendances) -// ========================================================= -// USAGE : node test-alerts.js -// ========================================================= - -import dotenv from 'dotenv'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// Charger les variables d'environnement -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -dotenv.config({ path: path.resolve(__dirname, '.env') }); - -// ========================================================= -// INITIALISATION DU MODULE -// ========================================================= -import db from './config/db.js'; -import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js'; -import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; -import { createAlertsService } from './modules/alerts/alerts.service.js'; - -// 1. Créer l'adapter MySQL avec la connexion DB -const mysqlAdapter = createMySQLAdapter(db); - -// 2. Créer le repository avec l'adapter -const alertsRepo = createAlertsRepo(mysqlAdapter); - -// 3. Créer le service avec le repository -const alertsService = createAlertsService(alertsRepo); - -// ========================================================= -// FONCTION DE TEST PRINCIPALE -// ========================================================= -async function runTest() { - console.log('\n' + '═'.repeat(80)); - console.log('TEST DU MODULE ALERTS'); - console.log('═'.repeat(80) + '\n'); - - try { - // ========================================================= - // ÉTAPE 1 : Vérifier la connexion DB - // ========================================================= - console.log('Test de connexion à la base de données\n'); - - // Utilise le repository créé avec injection de dépendances - const rules = await alertsRepo.findActiveRulesForSignal('test', 1); - console.log(`Connexion DB OK`); - console.log(` Règles trouvées : ${rules.length}\n`); - - if (rules.length === 0) { - console.log('ATTENTION : Aucune règle trouvée en DB'); - } - - // ========================================================= - // ÉTAPE 2 : Test du canal CONSOLE - // ========================================================= - console.log('─'.repeat(80)); - console.log('Test du canal CONSOLE\n'); - - const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js'); - - const testSignalConsole = { - action: 'BUY', - pair: 'BTC/EUR', - confidence: 0.87, - criticality: 'WARNING', - reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.' - }; - - await sendConsoleAlert(testSignalConsole); - console.log('Test CONSOLE réussi\n'); - - // ========================================================= - // ÉTAPE 3 : Test du canal EMAIL - // ========================================================= - console.log('─'.repeat(80)); - console.log('Test du canal EMAIL\n'); - - if (!process.env.MAIL_USER || !process.env.MAIL_PASS) { - console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)'); - console.log(' Pour tester configurez vos variables dans .env\n'); - } else { - console.log('Configuration email détectée'); - console.log(` Host : ${process.env.MAIL_HOST}`); - console.log(` User : ${process.env.MAIL_USER}`); - console.log(' Envoi d\'un email de test...\n'); - - const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js'); - - const testSignalEmail = { - action: 'SELL', - pair: 'ETH/EUR', - confidence: 0.92, - criticality: 'CRITICAL', - reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.' - }; - - const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail); - - if (status === 'SENT') { - console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n'); - } else { - console.log('Test EMAIL échoué. Vérifiez votre configuration.\n'); - } - } - - // ========================================================= - // ÉTAPE 4 : Test du service complet - // ========================================================= - if (rules.length > 0) { - console.log('─'.repeat(80)); - console.log('Test du service complet\n'); - - const testSignalService = { - userId: rules[0].user_id, - pairId: rules[0].pair_id || 1, - pair: 'BTC/EUR', - action: 'BUY', - confidence: 0.85, - criticality: 'INFO', - reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.', - priceAtSignal: 45000.50 - }; - - // Utilise le service créé avec injection de dépendances - await alertsService.processSignal(testSignalService); - console.log('Test SERVICE complet réussi !\n'); - } - - // ========================================================= - // RÉSUMÉ FINAL - // ========================================================= - console.log('═'.repeat(80)); - console.log('TOUS LES TESTS SONT TERMINÉS'); - console.log('═'.repeat(80)); - console.log('\n RÉSUMÉ :'); - console.log(' Connexion DB'); - console.log(' Canal CONSOLE'); - console.log(` ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`); - console.log(` ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`); - - } catch (error) { - console.error('\n ERREUR LORS DU TEST :'); - console.error(error); - console.error('\n VÉRIFICATIONS :'); - console.error(' - Le fichier .env existe et contient les bonnes valeurs ?'); - console.error(' - La base de données est démarrée ?'); - console.error(' - Les tables sont créées (schema.sql) ?'); - console.error('\n'); - } finally { - // Quitter proprement - process.exit(); - } -} - -// ========================================================= -// LANCEMENT DU TEST -// ========================================================= -runTest(); +// ========================================================= +// SCRIPT DE TEST - Module Alerts RÉUTILISABLE +// ========================================================= +// Ce script teste ton module d'alertes avec la nouvelle +// architecture réutilisable (injection de dépendances) +// ========================================================= +// USAGE : node test-alerts.js +// ========================================================= + +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Charger les variables d'environnement +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +// ========================================================= +// INITIALISATION DU MODULE +// ========================================================= +import db from './config/db.js'; +import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js'; +import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; +import { createAlertsService } from './modules/alerts/alerts.service.js'; + +// 1. Créer l'adapter MySQL avec la connexion DB +const mysqlAdapter = createMySQLAdapter(db); + +// 2. Créer le repository avec l'adapter +const alertsRepo = createAlertsRepo(mysqlAdapter); + +// 3. Créer le service avec le repository +const alertsService = createAlertsService(alertsRepo); + +// ========================================================= +// FONCTION DE TEST PRINCIPALE +// ========================================================= +async function runTest() { + console.log('\n' + '═'.repeat(80)); + console.log('TEST DU MODULE ALERTS'); + console.log('═'.repeat(80) + '\n'); + + try { + // ========================================================= + // ÉTAPE 1 : Vérifier la connexion DB + // ========================================================= + console.log('Test de connexion à la base de données\n'); + + // Utilise le repository créé avec injection de dépendances + const rules = await alertsRepo.findActiveRulesForSignal('test', 1); + console.log(`Connexion DB OK`); + console.log(` Règles trouvées : ${rules.length}\n`); + + if (rules.length === 0) { + console.log('ATTENTION : Aucune règle trouvée en DB'); + } + + // ========================================================= + // ÉTAPE 2 : Test du canal CONSOLE + // ========================================================= + console.log('─'.repeat(80)); + console.log('Test du canal CONSOLE\n'); + + const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js'); + + const testSignalConsole = { + action: 'BUY', + pair: 'BTC/EUR', + confidence: 0.87, + criticality: 'WARNING', + reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.' + }; + + await sendConsoleAlert(testSignalConsole); + console.log('Test CONSOLE réussi\n'); + + // ========================================================= + // ÉTAPE 3 : Test du canal EMAIL + // ========================================================= + console.log('─'.repeat(80)); + console.log('Test du canal EMAIL\n'); + + if (!process.env.MAIL_USER || !process.env.MAIL_PASS) { + console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)'); + console.log(' Pour tester configurez vos variables dans .env\n'); + } else { + console.log('Configuration email détectée'); + console.log(` Host : ${process.env.MAIL_HOST}`); + console.log(` User : ${process.env.MAIL_USER}`); + console.log(' Envoi d\'un email de test...\n'); + + const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js'); + + const testSignalEmail = { + action: 'SELL', + pair: 'ETH/EUR', + confidence: 0.92, + criticality: 'CRITICAL', + reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.' + }; + + const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail); + + if (status === 'SENT') { + console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n'); + } else { + console.log('Test EMAIL échoué. Vérifiez votre configuration.\n'); + } + } + + // ========================================================= + // ÉTAPE 4 : Test du service complet + // ========================================================= + if (rules.length > 0) { + console.log('─'.repeat(80)); + console.log('Test du service complet\n'); + + const testSignalService = { + userId: rules[0].user_id, + pairId: rules[0].pair_id || 1, + pair: 'BTC/EUR', + action: 'BUY', + confidence: 0.85, + criticality: 'INFO', + reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.', + priceAtSignal: 45000.50 + }; + + // Utilise le service créé avec injection de dépendances + await alertsService.processSignal(testSignalService); + console.log('Test SERVICE complet réussi !\n'); + } + + // ========================================================= + // RÉSUMÉ FINAL + // ========================================================= + console.log('═'.repeat(80)); + console.log('TOUS LES TESTS SONT TERMINÉS'); + console.log('═'.repeat(80)); + console.log('\n RÉSUMÉ :'); + console.log(' Connexion DB'); + console.log(' Canal CONSOLE'); + console.log(` ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`); + console.log(` ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`); + + } catch (error) { + console.error('\n ERREUR LORS DU TEST :'); + console.error(error); + console.error('\n VÉRIFICATIONS :'); + console.error(' - Le fichier .env existe et contient les bonnes valeurs ?'); + console.error(' - La base de données est démarrée ?'); + console.error(' - Les tables sont créées (schema.sql) ?'); + console.error('\n'); + } finally { + // Quitter proprement + process.exit(); + } +} + +// ========================================================= +// LANCEMENT DU TEST +// ========================================================= +runTest(); diff --git a/Wallette/server/modules/init-alerts.js b/Wallette/server/modules/init-alerts.js index 4f927b9..8e78b7a 100644 --- a/Wallette/server/modules/init-alerts.js +++ b/Wallette/server/modules/init-alerts.js @@ -1,134 +1,134 @@ -// ========================================================= -// INITIALISATION DU MODULE ALERTS -// ========================================================= -// Ce fichier montre comment initialiser et utiliser -// le module d'alertes dans Wall-e-tte -// ========================================================= - -import db from '../config/db.js'; -import { - createAlertsController, - createAlertsRepo, - createAlertsRouter, - createAlertsService, - createMySQLAdapter -} from './alerts/index.js'; - -// ========================================================= -// FONCTION D'INITIALISATION -// ========================================================= -/** - * Initialise tout le module d'alertes - * - * @param {Object} dbConnection - Connexion à la base de données - * @param {Object} options - Options de configuration - * @returns {Object} { repo, service, controller, router } - */ -export function initializeAlertsModule(dbConnection = db, options = {}) { - - console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne - console.log('INITIALISATION DU MODULE ALERTS'); - console.log('═'.repeat(80) + '\n'); - - // ───────────────────────────────────────────────────── - // ÉTAPE 1 : Créer l'Adapter MySQL - // ───────────────────────────────────────────────────── - // L'adapter contient les requêtes SQL spécifiques - const adapter = createMySQLAdapter(dbConnection, { - alertsTable: options.alertsTable || 'alert_rules', - usersTable: options.usersTable || 'users', - eventsTable: options.eventsTable || 'alert_events' - }); - - // ───────────────────────────────────────────────────── - // ÉTAPE 2 : Créer le Repository - // ───────────────────────────────────────────────────── - // Le repository gère les requêtes SQL - const alertsRepo = createAlertsRepo(adapter, { - alertsTable: options.alertsTable || 'alert_rules', - usersTable: options.usersTable || 'users', - eventsTable: options.eventsTable || 'alert_events' - }); - - // ───────────────────────────────────────────────────── - // ÉTAPE 3 : Créer le Service - // ───────────────────────────────────────────────────── - // Le service contient la logique métier - const alertsService = createAlertsService(alertsRepo, { - defaultCooldown: options.defaultCooldown || 60000 - }); - - // ───────────────────────────────────────────────────── - // ÉTAPE 4 : Créer le Controller - // ───────────────────────────────────────────────────── - // Le controller gère les requêtes HTTP - // (Plus besoin de db car le CRUD passe par le repo) - const alertsController = createAlertsController(alertsService, alertsRepo); - - // ───────────────────────────────────────────────────── - // ÉTAPE 5 : Créer le Router - // ───────────────────────────────────────────────────── - // Le router définit les routes API - const alertsRouter = createAlertsRouter(alertsController); - - console.log('Module Alerts complètement initialisé !'); - console.log('═'.repeat(80) + '\n'); - - // ───────────────────────────────────────────────────── - // RETOURNER TOUT - // ───────────────────────────────────────────────────── - return { - adapter, - repo: alertsRepo, - service: alertsService, - controller: alertsController, - router: alertsRouter - }; -} - -// ========================================================= -// EXPORT PAR DÉFAUT -// ========================================================= -// Pour Wall-e-tte, initialiser avec la DB par défaut -export default initializeAlertsModule(); - -// ========================================================= -// EXEMPLE D'UTILISATION -// ========================================================= -/* - UTILISATION SIMPLE (dans Wall-e-tte) : - - // Option 1 : Import par défaut (utilise la DB par défaut) - import alerts from './modules/init-alerts.js'; - - app.use('/api/alerts', alerts.router); - await alerts.service.processSignal(signal); - -================================================================================ - - // Option 2 : Import avec configuration personnalisée - import { initializeAlertsModule } from './modules/init-alerts.js'; - import myDb from './my-db.js'; - - const alerts = initializeAlertsModule(myDb, { - alertsTable: 'mes_alertes', - defaultCooldown: 120000 // 2 minutes - }); - - app.use('/api/alerts', alerts.router); - -================================================================================ - - UTILISATION DANS UN AUTRE PROJET : - - // Copier ce fichier et changer juste l'import de 'db' - import strangerDb from './stranger-db-config.js'; - import { - createAlertsRepo, - createAlertsService - } from '@wall-e-tte/alerts'; - - const repo = createAlertsRepo(strangerDb); - const service = createAlertsService(repo); - -*/ +// ========================================================= +// INITIALISATION DU MODULE ALERTS +// ========================================================= +// Ce fichier montre comment initialiser et utiliser +// le module d'alertes dans Wall-e-tte +// ========================================================= + +import db from '../config/db.js'; +import { + createAlertsController, + createAlertsRepo, + createAlertsRouter, + createAlertsService, + createMySQLAdapter +} from './alerts/index.js'; + +// ========================================================= +// FONCTION D'INITIALISATION +// ========================================================= +/** + * Initialise tout le module d'alertes + * + * @param {Object} dbConnection - Connexion à la base de données + * @param {Object} options - Options de configuration + * @returns {Object} { repo, service, controller, router } + */ +export function initializeAlertsModule(dbConnection = db, options = {}) { + + console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne + console.log('INITIALISATION DU MODULE ALERTS'); + console.log('═'.repeat(80) + '\n'); + + // ───────────────────────────────────────────────────── + // ÉTAPE 1 : Créer l'Adapter MySQL + // ───────────────────────────────────────────────────── + // L'adapter contient les requêtes SQL spécifiques + const adapter = createMySQLAdapter(dbConnection, { + alertsTable: options.alertsTable || 'alert_rules', + usersTable: options.usersTable || 'users', + eventsTable: options.eventsTable || 'alert_events' + }); + + // ───────────────────────────────────────────────────── + // ÉTAPE 2 : Créer le Repository + // ───────────────────────────────────────────────────── + // Le repository gère les requêtes SQL + const alertsRepo = createAlertsRepo(adapter, { + alertsTable: options.alertsTable || 'alert_rules', + usersTable: options.usersTable || 'users', + eventsTable: options.eventsTable || 'alert_events' + }); + + // ───────────────────────────────────────────────────── + // ÉTAPE 3 : Créer le Service + // ───────────────────────────────────────────────────── + // Le service contient la logique métier + const alertsService = createAlertsService(alertsRepo, { + defaultCooldown: options.defaultCooldown || 60000 + }); + + // ───────────────────────────────────────────────────── + // ÉTAPE 4 : Créer le Controller + // ───────────────────────────────────────────────────── + // Le controller gère les requêtes HTTP + // (Plus besoin de db car le CRUD passe par le repo) + const alertsController = createAlertsController(alertsService, alertsRepo); + + // ───────────────────────────────────────────────────── + // ÉTAPE 5 : Créer le Router + // ───────────────────────────────────────────────────── + // Le router définit les routes API + const alertsRouter = createAlertsRouter(alertsController); + + console.log('Module Alerts complètement initialisé !'); + console.log('═'.repeat(80) + '\n'); + + // ───────────────────────────────────────────────────── + // RETOURNER TOUT + // ───────────────────────────────────────────────────── + return { + adapter, + repo: alertsRepo, + service: alertsService, + controller: alertsController, + router: alertsRouter + }; +} + +// ========================================================= +// EXPORT PAR DÉFAUT +// ========================================================= +// Pour Wall-e-tte, initialiser avec la DB par défaut +export default initializeAlertsModule(); + +// ========================================================= +// EXEMPLE D'UTILISATION +// ========================================================= +/* + UTILISATION SIMPLE (dans Wall-e-tte) : + + // Option 1 : Import par défaut (utilise la DB par défaut) + import alerts from './modules/init-alerts.js'; + + app.use('/api/alerts', alerts.router); + await alerts.service.processSignal(signal); + +================================================================================ + + // Option 2 : Import avec configuration personnalisée + import { initializeAlertsModule } from './modules/init-alerts.js'; + import myDb from './my-db.js'; + + const alerts = initializeAlertsModule(myDb, { + alertsTable: 'mes_alertes', + defaultCooldown: 120000 // 2 minutes + }); + + app.use('/api/alerts', alerts.router); + +================================================================================ + + UTILISATION DANS UN AUTRE PROJET : + + // Copier ce fichier et changer juste l'import de 'db' + import strangerDb from './stranger-db-config.js'; + import { + createAlertsRepo, + createAlertsService + } from '@wall-e-tte/alerts'; + + const repo = createAlertsRepo(strangerDb); + const service = createAlertsService(repo); + +*/ -- 2.50.1